From f3c870553d8606360c02a8a0609263421689b851 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Mon, 13 Apr 2026 23:02:24 +0900 Subject: [PATCH 01/21] docs: update SRS tagging requirements --- .../Data/REQ-003_portfolio-required-fields.md | 167 ++++++++++++++++++ .../REQ-002_portfolio-tagging-and-search.md | 65 +++++++ docs/SRS/index.md | 10 +- 3 files changed, 237 insertions(+), 5 deletions(-) diff --git a/docs/SRS/Data/REQ-003_portfolio-required-fields.md b/docs/SRS/Data/REQ-003_portfolio-required-fields.md index b67e658..0e52f2f 100644 --- a/docs/SRS/Data/REQ-003_portfolio-required-fields.md +++ b/docs/SRS/Data/REQ-003_portfolio-required-fields.md @@ -8,6 +8,7 @@ ## Intent - `portfolio` 데이터가 운용 및 검색 맥락에서 일관되게 해석될 수 있도록 필수 필드와 저장 규칙을 명확히 정의한다. - 종목 이름, 심볼, 거래소, 시장 정보를 표준화된 스키마로 관리하여 API, CLI, DB 간 데이터 계약 불일치를 줄인다. +- 태그 기반으로 포트폴리오 목록을 소비하는 외부 서비스가 필요로 하는 원천 데이터 필드 계약을 정확히 정의한다. ## Requirement - 시스템은 신규로 생성되거나 수정되는 각 `portfolio` 레코드에 대해 `name`, `symbol`, `exchange`, `market` 필드를 데이터 계약상 포함해야 한다. @@ -18,6 +19,8 @@ - 시스템은 복수 거래소 지원을 위해 `exchange`를 복수 문자열을 표현할 수 있는 데이터 구조로 저장해야 한다. - 시스템은 기존 `portfolio` 레코드에 `exchange` 또는 `market` 값이 누락되어 있더라도 마이그레이션 과정에서 `NULL` 또는 빈 문자열(`""`)로 보정할 수 있어야 한다. - 시스템은 필수 필드 계약이 추가된 이후에도 기존 레코드 조회가 실패하지 않도록 하위 호환성을 유지해야 한다. +- 시스템은 태그 기반 포트폴리오 목록 조회 응답에서 각 항목의 최소 필드(`symbol`, `name`, `exchange`, `market`, `updated_at`)를 제공해야 한다. +- 시스템은 태그 필터 파라미터를 사용한 목록 조회에서 태그 등록/검색 규칙을 REQ-002 계약에 따라 처리해야 한다. ## Acceptance Criteria 1. Given 새 포트폴리오 레코드가 생성되거나 기존 포트폴리오가 수정될 때, When 시스템이 데이터를 저장하면, Then 해당 레코드는 `name`, `symbol`, `exchange`, `market` 필드를 포함하는 구조로 저장되거나 반환되어야 한다. @@ -25,6 +28,8 @@ 3. Given 사용자가 enum에 없는 새로운 거래소 문자열 또는 시장 문자열을 제공할 때, When 시스템이 이를 저장하면, Then 시스템은 값 목록 제한만을 이유로 해당 값을 거부하지 않아야 한다. 4. Given 기존 포트폴리오 레코드에 `exchange` 또는 `market` 값이 없는 상태에서 스키마가 적용될 때, When 마이그레이션 또는 후속 조회가 수행되면, Then 시스템은 누락 값을 `NULL` 또는 빈 문자열(`""`)로 처리하고 레코드 조회를 실패시키지 않아야 한다. 5. Given 기존 포트폴리오 레코드가 이미 `name`과 `symbol`을 포함하고 있을 때, When 새 필드 계약이 적용된 후 데이터를 조회하면, Then 기존 `name`과 `symbol` 값은 변경되지 않고 그대로 반환되어야 한다. +6. Given 소비 서비스가 태그 기반 목록 조회를 수행하는 상태, When 응답 항목을 확인하면, Then 각 항목에는 최소 `symbol`, `name`, `exchange`, `market`, `updated_at` 필드가 포함되어야 한다. +7. Given 소비 서비스가 태그 필터를 포함해 목록 조회를 요청한 상태, When 조회를 수행하면, Then 태그 필터 적용 규칙은 REQ-002에서 정의한 include/exclude 및 매칭 규칙을 따라야 한다. ## Impacted Area - Docs: `docs/db-schema-spec.md`, `docs/portfoliohub-spec.md`, `docs/api-examples.md`, `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md` @@ -35,9 +40,171 @@ - Assumption: 복수 거래소 지원은 별도 정규화 테이블보다 `exchange` 배열 컬럼이 현재 범위에 더 단순하고 적합한 권장 방식이다. - 신규 생성/수정 계약에서는 `name`, `symbol`, `exchange`, `market` 필드를 모두 포함하되, 기존 데이터 호환성을 위해 레거시 레코드의 누락 신규 필드는 `NULL` 또는 `""`로 허용한다. - `exchange`와 `market`은 허용값 enum을 두지 않고 자유 문자열로 저장한다. +- Note: 본 requirement는 portfolio-hub의 원천 데이터 제공 계약만 다루며, 조회 결과를 스냅샷화하거나 캐시/복구하는 책임은 소비 서비스에 있다. +- Note: 조회 결과의 결정성(동일 tag 필터 입력에 대한 동일 응답 항목 집합 및 정렬 순서) 유지는 소비 서비스(예: stock-picker)의 책임이다. portfolio-hub는 tag 필터를 적용하여 목록을 반환하기만 한다. +- Note: 태그 등록과 태그 필터 세부 규칙은 REQ-002를 단일 source of truth로 사용한다. ## Change Log +### CHG-20260413-07 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- REQ-003에서 tag 필터 기반 조회의 결정성(동일 항목 집합/정렬) 유지 책임을 portfolio-hub에서 제거하고 stock-picker(소비 서비스)로 이관했다. + +#### Intent +- portfolio-hub의 책임을 "tag 필터 적용 후 목록 반환"으로 최소화하고, 조회 결과의 캐시/복구/결정성 추적은 소비 서비스가 담당하도록 명확히 하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 없음 +- Updated: + - Requirement에서 "동일 tag 필터 조건에서 항목 집합과 정렬 기준을 결정적으로 유지" 요구사항 제거 + - Notes에 결정성 책임이 소비 서비스에 있음을 명시 +- Removed: + - Acceptance Criteria #7 (결정성 검증 기준) + - Requirement의 결정성 유지 조항 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Data/REQ-003_portfolio-required-fields.md` + - `docs/SRS/index.md` +- Modules: + - 없음 +- Tests: + - 없음 + +#### Validation Plan +- REQ-003의 tag 필터 기반 목록 조회 응답이 필수 필드와 REQ-002 필터 규칙을 만족하는지로 검증한다. +- 결정성 검증(동일 tag 필터 입력에 대한 동일 응답)은 stock-picker 테스트에서 담당한다. + +### CHG-20260413-06 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- REQ-003에서 `sourceVersion`/`sourceTimestamp` 메타데이터 요구를 제거하고 태그 필터 기반 조회 결정성 계약만 유지했다. + +#### Intent +- portfolio-hub가 특정 소비자 전용 메타데이터를 제공하지 않고도 공용 태그 기반 목록 조회 계약을 만족하도록 경계를 명확히 하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 없음 +- Updated: + - Requirement의 조회 결정성 기준을 태그 필터 조건 기준으로 정리 + - Acceptance Criteria에서 source metadata 검증 항목 제거 및 결정성 검증 항목 재정의 +- Removed: + - `sourceVersion`/`sourceTimestamp` 제공 검증 기준 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Data/REQ-003_portfolio-required-fields.md` + - `docs/SRS/index.md` +- Modules: + - 없음 +- Tests: + - 없음 + +#### Validation Plan +- 동일 태그 필터 조건으로 반복 조회 시 항목 집합/정렬이 동일한지로 계약 충족 여부를 검증한다. + +### CHG-20260413-05 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- REQ-003의 stock-picker 전용 표현을 소비 서비스 중립 표현으로 정리했다. + +#### Intent +- portfolio-hub가 특정 소비자 구현을 인지하지 않아도 되는 일반 태그 기반 목록 조회 계약으로 요구사항 경계를 명확히 하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 없음 +- Updated: + - Intent/Requirement/Acceptance Criteria의 소비자 표현을 일반화 + - Notes의 책임 주체를 특정 서비스명 대신 소비 서비스로 일반화 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Data/REQ-003_portfolio-required-fields.md` + - `docs/SRS/index.md` +- Modules: + - 없음 +- Tests: + - 없음 + +#### Validation Plan +- REQ-003 본문에서 특정 소비자 고정 표현 없이 태그 기반 조회 계약이 동일하게 해석되는지 문서 리뷰로 확인한다. + +### CHG-20260413-04 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- REQ-003에 stock-picker 태그 필터 조회 시 REQ-002 계약을 따른다는 교차 참조를 추가했다. + +#### Intent +- 원천 데이터 필드 계약과 태그 필터 계약의 문서 경계를 명확히 분리하고 추적성을 강화하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 태그 필터 조회가 REQ-002 규칙을 따르는지 검증하는 기준 +- Updated: + - Requirement에 REQ-002 교차 참조 조항 추가 + - Notes에 태그 계약 source of truth 명시 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Data/REQ-003_portfolio-required-fields.md` + - `docs/SRS/index.md` +- Modules: + - 없음 +- Tests: + - 태그 필터 조회 계약 연계 테스트 항목 추가 + +#### Validation Plan +- REQ-003과 REQ-002 간 참조가 충돌 없이 추적 가능한지 문서 리뷰로 확인한다. + +### CHG-20260413-03 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- stock-picker 기본 유니버스 소비를 위한 원천 데이터 필드/버전 메타데이터/조회 정합성 최소 계약을 REQ-003에 추가했다. + +#### Intent +- portfolio-hub 책임을 원천 데이터 계약으로 한정하면서 소비자(stock-picker)의 입력 고정 요구를 지원한다. + +#### Acceptance Criteria Delta +- Added: + - 원천 데이터 조회 필수 필드 검증 기준 + - sourceVersion/sourceTimestamp 메타데이터 검증 기준 + - 동일 sourceVersion 조회 정합성 검증 기준 +- Updated: + - Requirement에 stock-picker 원천 데이터 제공 계약 조항 추가 + - Notes에 책임 분리 문구 추가 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - REQ-003 원천 데이터 계약 보강 +- Modules: + - portfolio list response serializer 및 metadata 노출 경로 영향 식별 +- Tests: + - source metadata 및 deterministic ordering 검증 테스트 항목 추가 + +#### Validation Plan +- 동일 sourceVersion 기준 조회 2회 결과의 항목 집합/정렬 일치 여부를 검증한다. +- 원천 데이터 조회 응답에 필수 필드와 source metadata가 존재하는지 확인한다. + ### CHG-20260410-02 - Date: 2026-04-10 - Author: GitHub Copilot diff --git a/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md b/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md index 2607df1..6d175a7 100644 --- a/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md +++ b/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md @@ -13,6 +13,8 @@ - 시스템은 각 포트폴리오에 대해 하나 이상의 태그를 추가하고 제거할 수 있어야 한다. - 시스템은 HTTP API와 CLI를 통해 동일한 포트폴리오 태깅 기능을 제공해야 한다. - 시스템은 둘 이상의 태그를 조건으로 포트폴리오를 검색할 수 있어야 하며, 검색 요청에서 `AND` 또는 `OR` 매칭 방식을 선택할 수 있어야 한다. +- 시스템은 다운스트림 소비자가 태그 기반 목록 조회 시 사용할 수 있도록 포함 태그(`includeTags`)와 제외 태그(`excludeTags`)를 동시에 지원해야 한다. +- 시스템은 태그 기반 조회에서 `grade:*`, `blacklist:*`와 같은 태그 네임스페이스를 그대로 보존하고 필터 조건으로 사용할 수 있어야 한다. - 시스템은 공백만으로 이루어지지 않은 태그 문자열을 추가 대상으로 받아들여야 한다. - 시스템은 동일 포트폴리오에 이미 연결된 동일한 태그 문자열을 다시 추가하려는 요청에 대해 `409 Conflict` 또는 이에 대응하는 CLI 오류를 반환해야 하며, 기존 태그 상태를 변경하지 않아야 한다. - 시스템은 태그 추가·삭제·검색 요청에 대해 기존 포트폴리오 접근 권한 정책을 적용해야 하며, 인증 실패 또는 권한 부족 시 기존 인증 오류 계약에 따라 요청을 거부해야 한다. @@ -30,6 +32,7 @@ 7. Given 존재하지 않는 `portfolio_id`가 요청에 포함될 때, When 사용자가 API 또는 CLI로 태그 추가 또는 삭제를 시도하면, Then 시스템은 `404` 응답 또는 이에 대응하는 CLI 오류를 반환하고 어떤 태그 연결도 생성·변경하지 않아야 한다. 8. Given 포트폴리오에 이미 여러 태그가 연결되어 있고 사용자가 긴 태그 문자열을 추가할 때, When API 또는 CLI로 태그 추가를 요청하면, Then 시스템은 애플리케이션 정의 최대 길이 제한 또는 최대 개수 제한만을 이유로 요청을 거부하지 않아야 한다. 9. Given `OR` 모드의 다중 태그 검색에서 하나의 포트폴리오가 두 개 이상의 요청 태그와 동시에 일치할 때, When 사용자가 검색을 수행하면, Then 시스템은 해당 포트폴리오를 결과에 한 번만 반환해야 한다. +10. Given 소비 서비스가 `includeTags=["grade:A", "grade:B"]`, `excludeTags=["blacklist:trade"]` 조건으로 목록 조회를 요청한 상태, When 태그 필터 검색을 수행하면, Then 시스템은 포함 조건을 만족하고 제외 조건을 만족하지 않는 항목만 반환해야 한다. ## Impacted Area - Docs: `docs/portfoliohub-spec.md`, `docs/api-examples.md`, `docs/db-schema-spec.md`, `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md` @@ -40,9 +43,71 @@ - 본 requirement의 검색 범위는 다중 태그 검색을 포함하며, 호출자는 `AND` 또는 `OR` 모드를 선택할 수 있다. - 태그 최대 길이와 포트폴리오당 최대 태그 개수에 대한 애플리케이션 정의 상한은 두지 않는다. - 동일 포트폴리오에 같은 태그 문자열을 다시 추가하는 요청은 `409 Conflict` 또는 이에 대응하는 CLI 오류로 처리한다. +- 특정 소비 서비스의 입력 고정/스냅샷 책임은 소비 서비스 측 requirement에서 관리하며, portfolio-hub는 태그 등록/검색 계약만 제공한다. ## Change Log +### CHG-20260413-04 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- REQ-002의 stock-picker 특정 소비자 표현을 다운스트림 소비자 일반 계약으로 정리했다. + +#### Intent +- portfolio-hub 태그 검색 기능을 특정 소비자 전용 기능이 아닌 공용 인터페이스 계약으로 유지하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 없음 +- Updated: + - Requirement/Acceptance Criteria/Notes의 소비자 표현 일반화 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` + - `docs/SRS/index.md` +- Modules: + - 없음 +- Tests: + - 없음 + +#### Validation Plan +- REQ-002 본문이 특정 소비자 구현을 전제하지 않고 include/exclude 태그 검색 계약만 명확히 정의하는지 확인한다. + +### CHG-20260413-03 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- REQ-002에 stock-picker 기본 유니버스 소비를 위한 include/exclude 태그 필터 계약을 추가했다. + +#### Intent +- portfolio-hub 태그 등록값(예: grade, blacklist)을 downstream 필터 인자로 직접 활용하는 계약을 명시하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - includeTags/excludeTags 동시 적용 검증 기준 +- Updated: + - Requirement에 stock-picker 소비자 관점 필터 계약 추가 + - Notes에 stock-picker 연계 경계 문구 추가 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` + - `docs/SRS/index.md` +- Modules: + - portfolio tag search API/CLI query parser 영향 식별 +- Tests: + - include/exclude 조합 필터링 테스트 항목 추가 + +#### Validation Plan +- includeTags/excludeTags 조합 요청에서 반환 집합이 예상과 일치하는지 API/CLI 테스트로 확인한다. + ### CHG-20260410-02 - Date: 2026-04-10 - Author: GitHub Copilot diff --git a/docs/SRS/index.md b/docs/SRS/index.md index 78f3898..c45327e 100644 --- a/docs/SRS/index.md +++ b/docs/SRS/index.md @@ -26,11 +26,11 @@ | REQ-003 | 포트폴리오 필수 필드 및 거래소/시장 데이터 정의 | Data | Proposed | Medium | Justin | `docs/SRS/Data/REQ-003_portfolio-required-fields.md` | ## Recent Change Log Summary -- 2026-04-10 — `REQ-003`에 신규 입력 계약과 레거시 호환성 규칙을 분리해 반영했다. -- 2026-04-10 — `REQ-003` 초안을 추가해 포트폴리오 필수 필드와 거래소/시장 데이터 계약을 정의했다. -- 2026-04-10 — `REQ-002`에 다중 태그 `AND`/`OR` 검색, 중복 추가 `409 Conflict`, 태그 길이/개수 상한 없음 계약을 반영했다. -- 2026-04-10 — `REQ-002` 초안을 추가해 포트폴리오 태깅과 태그 기반 검색의 API/CLI 계약을 정의했다. -- 2026-04-10 — `REQ-001` 상태를 `Approved`로 전환했다. +- 2026-04-13 — `REQ-003`에서 태그 필터 기반 조회의 결정성 유지 책임을 소비 서비스(stock-picker)로 이관했다. +- 2026-04-13 — `REQ-003`에서 `sourceVersion`/`sourceTimestamp` 요구를 제거하고 태그 필터 기반 조회 결정성 계약으로 정리했다. +- 2026-04-13 — `REQ-003`의 소비자 표현을 특정 서비스 전용에서 태그 기반 조회 소비 서비스 일반 계약으로 정리했다. +- 2026-04-13 — `REQ-002`의 소비자 표현을 특정 서비스 전용에서 다운스트림 소비자 일반 계약으로 정리했다. +- 2026-04-13 — `REQ-002`에 includeTags/excludeTags 태그 필터 계약을 추가했다. ## Status Transition Evidence - `Proposed` → `Approved`: Owner가 지정되고 requirement 문서 리뷰가 완료되며 acceptance criteria가 누락 없이 검토되어야 한다. From 256b3709a0855a36d96c3a48ada38575e9706e2d Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Mon, 13 Apr 2026 23:20:25 +0900 Subject: [PATCH 02/21] refactor docs into technical specs --- README.md | 10 +- .../Data/REQ-003_portfolio-required-fields.md | 40 +- .../REQ-001_portfolio-comment-history.md | 38 +- .../REQ-002_portfolio-tagging-and-search.md | 38 +- docs/api-examples.md | 834 ------------------ docs/db-schema-spec.md | 332 ------- docs/deployment.md | 194 ---- docs/portfoliohub-spec.md | 697 --------------- docs/technical/api-contracts.md | 663 ++++++++++++++ docs/technical/architecture-overview.md | 113 +++ docs/technical/auth-and-token-lifecycle.md | 54 ++ docs/technical/deployment-and-operations.md | 193 ++++ docs/technical/index.md | 49 + docs/technical/management-cli-operations.md | 84 ++ docs/technical/persistence-and-migrations.md | 279 ++++++ docs/technical/project-structure-baseline.md | 39 + .../technical/python-runtime-and-packaging.md | 33 + docs/technical/settings-and-env-loading.md | 84 ++ scripts/check_docs_sync.py | 26 +- 19 files changed, 1713 insertions(+), 2087 deletions(-) delete mode 100644 docs/api-examples.md delete mode 100644 docs/db-schema-spec.md delete mode 100644 docs/deployment.md delete mode 100644 docs/portfoliohub-spec.md create mode 100644 docs/technical/api-contracts.md create mode 100644 docs/technical/architecture-overview.md create mode 100644 docs/technical/auth-and-token-lifecycle.md create mode 100644 docs/technical/deployment-and-operations.md create mode 100644 docs/technical/index.md create mode 100644 docs/technical/management-cli-operations.md create mode 100644 docs/technical/persistence-and-migrations.md create mode 100644 docs/technical/project-structure-baseline.md create mode 100644 docs/technical/python-runtime-and-packaging.md create mode 100644 docs/technical/settings-and-env-loading.md diff --git a/README.md b/README.md index 9a9eb10..95fec15 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ your-domain.com { } ``` -See **[docs/deployment.md](docs/deployment.md)** for a full guide covering: +See **[docs/technical/deployment-and-operations.md](docs/technical/deployment-and-operations.md)** for a full guide covering: - Caddy installation on Linux/Ubuntu - CORS configuration (`PORTFOLIOHUB_ALLOWED_ORIGINS`) @@ -152,10 +152,10 @@ To keep implementation and specification documents in sync, this repository prov Rules enforced: -- If files under `app/` change, `docs/portfoliohub-spec.md` must be updated in the same commit/PR. -- If files under `app/api/` or `app/schemas/` change, `docs/api-examples.md` must be updated in the same commit/PR. -- If files under `migrations/versions/` change, `docs/db-schema-spec.md` must be updated in the same commit/PR. -- Changed migration revision IDs must appear in `docs/db-schema-spec.md`. +- If files under `app/` change, at least one `docs/technical/` document must be updated in the same commit/PR. +- If files under `app/api/` or `app/schemas/` change, `docs/technical/api-contracts.md` must be updated in the same commit/PR. +- If files under `migrations/versions/` change, `docs/technical/persistence-and-migrations.md` must be updated in the same commit/PR. +- Changed migration revision IDs must appear in `docs/technical/persistence-and-migrations.md`. Enable local pre-commit check: diff --git a/docs/SRS/Data/REQ-003_portfolio-required-fields.md b/docs/SRS/Data/REQ-003_portfolio-required-fields.md index 0e52f2f..a6ad9c6 100644 --- a/docs/SRS/Data/REQ-003_portfolio-required-fields.md +++ b/docs/SRS/Data/REQ-003_portfolio-required-fields.md @@ -32,7 +32,7 @@ 7. Given 소비 서비스가 태그 필터를 포함해 목록 조회를 요청한 상태, When 조회를 수행하면, Then 태그 필터 적용 규칙은 REQ-002에서 정의한 include/exclude 및 매칭 규칙을 따라야 한다. ## Impacted Area -- Docs: `docs/db-schema-spec.md`, `docs/portfoliohub-spec.md`, `docs/api-examples.md`, `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md` +- Docs: `docs/technical/architecture-overview.md`, `docs/technical/api-contracts.md`, `docs/technical/persistence-and-migrations.md`, `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md` - Modules: `app/domain/models.py`, `app/schemas/portfolio.py`, `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py`, `app/services/portfolio_service.py`, `migrations/versions/*` - Tests: `tests/test_app.py`, `tests/test_domain_services.py` @@ -46,6 +46,38 @@ ## Change Log +### CHG-20260413-08 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- 삭제 예정인 상위 docs 경로를 technical 문서 경로로 정리했다. + +#### Intent +- 데이터 계약 requirement가 삭제 후에도 유효한 구현 추적 문서를 가리키도록 유지하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 없음 +- Updated: + - Impacted Area의 docs 경로를 `docs/technical/` 기준으로 갱신 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Data/REQ-003_portfolio-required-fields.md` + - `docs/technical/architecture-overview.md` + - `docs/technical/api-contracts.md` + - `docs/technical/persistence-and-migrations.md` +- Modules: + - 없음 +- Tests: + - 없음 + +#### Validation Plan +- 삭제 대상 문서 제거 후에도 REQ-003의 docs 추적 링크가 모두 유효한지 확인한다. + ### CHG-20260413-07 - Date: 2026-04-13 - Author: GitHub Copilot @@ -261,9 +293,9 @@ #### Impacted Area Delta - Docs: - - db-schema-spec.md - - portfoliohub-spec.md - - api-examples.md + - docs/technical/architecture-overview.md + - docs/technical/api-contracts.md + - docs/technical/persistence-and-migrations.md - index.md - traceability-matrix.md - Modules: diff --git a/docs/SRS/Interface/REQ-001_portfolio-comment-history.md b/docs/SRS/Interface/REQ-001_portfolio-comment-history.md index 59e04eb..c5645d7 100644 --- a/docs/SRS/Interface/REQ-001_portfolio-comment-history.md +++ b/docs/SRS/Interface/REQ-001_portfolio-comment-history.md @@ -30,7 +30,7 @@ 7. Given 존재하지 않는 `portfolio_id` 또는 `comment_id`가 요청에 포함될 때, When 사용자가 API 또는 CLI로 조회·수정·삭제를 시도하면, Then 시스템은 `404` 응답 또는 이에 대응하는 CLI 오류를 반환하고 새로운 메모를 생성하거나 기존 데이터를 변경하지 않아야 한다. ## Impacted Area -- Docs: `docs/portfoliohub-spec.md`, `docs/api-examples.md`, `docs/db-schema-spec.md`, `docs/SRS/index.md` +- Docs: `docs/technical/api-contracts.md`, `docs/technical/persistence-and-migrations.md`, `docs/SRS/index.md` - Modules: `app/api/portfolios.py`, `app/cli/commands/portfolio.py`, `app/schemas/portfolio.py`, `app/services/portfolio_service.py`, `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py`, `migrations/versions/*` - Tests: `tests/test_app.py`, `tests/test_cli.py`, `tests/test_domain_services.py` @@ -41,6 +41,37 @@ ## Change Log +### CHG-20260413-04 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- 삭제 예정인 상위 docs 경로를 technical 문서 경로로 정리했다. + +#### Intent +- 구현 추적 문서가 삭제 후에도 유효한 참조를 유지하도록 하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 없음 +- Updated: + - Impacted Area의 docs 경로를 `docs/technical/` 기준으로 갱신 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Interface/REQ-001_portfolio-comment-history.md` + - `docs/technical/api-contracts.md` + - `docs/technical/persistence-and-migrations.md` +- Modules: + - 없음 +- Tests: + - 없음 + +#### Validation Plan +- 삭제 대상 문서 제거 후에도 REQ-001의 구현 추적 문서 링크가 모두 유효한지 확인한다. + ### CHG-20260410-03 - Date: 2026-04-10 - Author: GitHub Copilot @@ -126,9 +157,8 @@ #### Impacted Area Delta - Docs: - - portfoliohub-spec.md - - api-examples.md - - db-schema-spec.md + - docs/technical/api-contracts.md + - docs/technical/persistence-and-migrations.md - `docs/SRS/index.md` - Modules: - portfolios.py diff --git a/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md b/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md index 6d175a7..5a58a03 100644 --- a/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md +++ b/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md @@ -35,7 +35,7 @@ 10. Given 소비 서비스가 `includeTags=["grade:A", "grade:B"]`, `excludeTags=["blacklist:trade"]` 조건으로 목록 조회를 요청한 상태, When 태그 필터 검색을 수행하면, Then 시스템은 포함 조건을 만족하고 제외 조건을 만족하지 않는 항목만 반환해야 한다. ## Impacted Area -- Docs: `docs/portfoliohub-spec.md`, `docs/api-examples.md`, `docs/db-schema-spec.md`, `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md` +- Docs: `docs/technical/api-contracts.md`, `docs/technical/persistence-and-migrations.md`, `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md` - Modules: `app/api/portfolios.py`, `app/cli/commands/portfolio.py`, `app/schemas/portfolio.py`, `app/services/portfolio_service.py`, `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py`, `app/domain/models.py`, `migrations/versions/*` - Tests: `tests/test_app.py`, `tests/test_cli.py`, `tests/test_domain_services.py` @@ -47,6 +47,37 @@ ## Change Log +### CHG-20260413-05 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- 삭제 예정인 상위 docs 경로를 technical 문서 경로로 정리했다. + +#### Intent +- 태그 기능 requirement가 삭제 후에도 유효한 구현 추적 문서를 가리키도록 유지하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 없음 +- Updated: + - Impacted Area의 docs 경로를 `docs/technical/` 기준으로 갱신 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` + - `docs/technical/api-contracts.md` + - `docs/technical/persistence-and-migrations.md` +- Modules: + - 없음 +- Tests: + - 없음 + +#### Validation Plan +- 삭제 대상 문서 제거 후에도 REQ-002의 docs 추적 링크가 모두 유효한지 확인한다. + ### CHG-20260413-04 - Date: 2026-04-13 - Author: GitHub Copilot @@ -165,9 +196,8 @@ #### Impacted Area Delta - Docs: - - portfoliohub-spec.md - - api-examples.md - - db-schema-spec.md + - docs/technical/api-contracts.md + - docs/technical/persistence-and-migrations.md - index.md - traceability-matrix.md - Modules: diff --git a/docs/api-examples.md b/docs/api-examples.md deleted file mode 100644 index 5ab64c1..0000000 --- a/docs/api-examples.md +++ /dev/null @@ -1,834 +0,0 @@ -# PortfolioHub API Examples - -## 1. Purpose - -This document contains example requests and responses for PortfolioHub HTTP APIs. - -- Sync rule: when files under `app/api/` or `app/schemas/` change, this document must be updated in the same commit/PR. -- Companion spec: `docs/portfoliohub-spec.md` - -## 2. Conventions - -- Base URL example: `http://localhost:8000` -- Auth header format: `Authorization: Bearer ` -- JSON examples are representative and may omit some optional fields. - -## 3. Health - -### 3.1 GET /health - -Request: - -```bash -curl -X GET "http://localhost:8000/health" -``` - -Response 200: - -```json -{ - "status": "ok" -} -``` - -## 4. Portfolios - -### 4.1 GET /portfolios - -Request: - -```bash -curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled" \ - -H "Authorization: Bearer " -``` - -Response 200: - -```json -[ - { - "portfolio_id": "krw-btc", - "broker": "upbit", - "status": "enabled", - "name": "Bitcoin", - "symbol": "KRW-BTC", - "cash_balance": 1000000.0, - "asset_quantity": 0.1, - "price": 100000000.0, - "profit_buffer": 0.0, - "active_strategy_id": "stv-001", - "avabile_strategies": ["stv-001"], - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T00:00:00Z", - "last_trade_at": "2026-04-03T00:00:00Z", - "total_value": 11000000.0 - } -] -``` - -### 4.2 GET /portfolios/{portfolio_id} - -Request: - -```bash -curl -X GET "http://localhost:8000/portfolios/krw-btc?recent_trades=5" \ - -H "Authorization: Bearer " -``` - -Response 200: - -```json -{ - "portfolio": { - "portfolio_id": "krw-btc", - "broker": "upbit", - "status": "enabled", - "name": "Bitcoin", - "symbol": "KRW-BTC", - "cash_balance": 1000000.0, - "asset_quantity": 0.1, - "price": 100000000.0, - "profit_buffer": 0.0, - "active_strategy_id": "stv-001", - "avabile_strategies": ["stv-001"], - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T00:00:00Z", - "last_trade_at": "2026-04-03T00:00:00Z" - }, - "recent_entries": [ - { - "portfolio_id": "krw-btc", - "recorded_at": "2026-04-03T00:00:00Z", - "action": "buy", - "price": 100000000.0, - "quantity": 0.1, - "fee_amount": 0.0, - "tax_amount": 0.0, - "cash_balance_after": 1000000.0, - "asset_quantity_after": 0.1, - "total_value_after": 11000000.0, - "profit_buffer_after": 0.0, - "strategy_set_id": "stv-001", - "note": "" - } - ], - "strategy_set": { - "id": "stv-001", - "name": "target_value", - "config": "{}", - "nick": "target_value:99914b932bd3", - "created_at": "2026-04-03T00:00:00Z" - }, - "live_quantity": null -} -``` - -### 4.3 POST /portfolios - -Request: - -```bash -curl -X POST "http://localhost:8000/portfolios" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "portfolio_id": "krw-btc", - "broker": "upbit", - "status": "enabled", - "name": "Bitcoin", - "symbol": "KRW-BTC", - "strategy_name": "target_value", - "strategy_params": {}, - "active_strategy_id": null, - "avabile_strategies": null, - "cash_balance": 1000000, - "asset_quantity": 0, - "asset_price": 0, - "profit_buffer": 0 - }' -``` - -Response 201: - -- Response body shape is `PortfolioDetailResponse` (same as 4.2). - -### 4.4 PATCH /portfolios/{portfolio_id} - -Request: - -```bash -curl -X PATCH "http://localhost:8000/portfolios/krw-btc" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "broker": "upbit", - "name": "Bitcoin", - "symbol": "KRW-BTC" - }' -``` - -Response 200: - -- Response body shape is `PortfolioDetailResponse`. - -### 4.5 POST /portfolios/{portfolio_id}/buy - -Request: - -```bash -curl -X POST "http://localhost:8000/portfolios/krw-btc/buy" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"quantity": 0.01, "price": 100000000, "fee": 0, "tax": 0, "note": ""}' -``` - -Response 200: - -- Response body shape is `PortfolioDetailResponse`. - -### 4.6 POST /portfolios/{portfolio_id}/sell - -Request: - -```bash -curl -X POST "http://localhost:8000/portfolios/krw-btc/sell" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"quantity": 0.01, "price": 100000000, "fee": 0, "tax": 0, "note": ""}' -``` - -Response 200: - -- Response body shape is `PortfolioDetailResponse`. - -### 4.7 POST /portfolios/{portfolio_id}/invest - -Request: - -```bash -curl -X POST "http://localhost:8000/portfolios/krw-btc/invest" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"movement_type": "deposit", "amount": 50000, "note": ""}' -``` - -Response 200: - -- Response body shape is `PortfolioDetailResponse`. - -### 4.8 POST /portfolios/{portfolio_id}/transfer - -Request: - -```bash -curl -X POST "http://localhost:8000/portfolios/krw-btc/transfer" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"direction": "to-buffer", "amount": 10000, "note": ""}' -``` - -Response 200: - -- Response body shape is `PortfolioDetailResponse`. - -### 4.9 POST /portfolios/{portfolio_id}/reset - -Request: - -```bash -curl -X POST "http://localhost:8000/portfolios/krw-btc/reset" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"infer_symbol": "KRW-BTC"}' -``` - -Response 200: - -- Response body shape is `PortfolioDetailResponse`. - -### 4.10 DELETE /portfolios/{portfolio_id} - -Request: - -```bash -curl -X DELETE "http://localhost:8000/portfolios/krw-btc?keep_ledger=false" \ - -H "Authorization: Bearer " -``` - -Response 204: - -- No content. - -### 4.11 POST /portfolios/{portfolio_id}/comments - -Create a new comment for a portfolio. - -Required permission: `trade-write` - -Request: - -```bash -curl -X POST "http://localhost:8000/portfolios/krw-btc/comments" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"content": "Bought more BTC today.\nLooking good."}' -``` - -Response 201: - -```json -{ - "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "portfolio_id": "krw-btc", - "content": "Bought more BTC today.\nLooking good.", - "created_at": "2026-04-10T00:00:00Z", - "updated_at": "2026-04-10T00:00:00Z" -} -``` - -### 4.12 GET /portfolios/{portfolio_id}/comments - -List all comments for a portfolio, sorted by most recent first. - -Required permission: `read` - -Request: - -```bash -curl -X GET "http://localhost:8000/portfolios/krw-btc/comments" \ - -H "Authorization: Bearer " -``` - -Response 200: - -```json -[ - { - "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "portfolio_id": "krw-btc", - "content": "Bought more BTC today.\nLooking good.", - "created_at": "2026-04-10T00:00:00Z", - "updated_at": "2026-04-10T00:00:00Z" - } -] -``` - -### 4.13 PATCH /portfolios/{portfolio_id}/comments/{comment_id} - -Update an existing comment's content. - -Required permission: `trade-write` - -Request: - -```bash -curl -X PATCH "http://localhost:8000/portfolios/krw-btc/comments/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{"content": "Updated note: BTC rally continues."}' -``` - -Response 200: - -```json -{ - "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "portfolio_id": "krw-btc", - "content": "Updated note: BTC rally continues.", - "created_at": "2026-04-10T00:00:00Z", - "updated_at": "2026-04-10T01:00:00Z" -} -``` - -### 4.14 DELETE /portfolios/{portfolio_id}/comments/{comment_id} - -Delete a comment. - -Required permission: `trade-write` - -Request: - -```bash -curl -X DELETE "http://localhost:8000/portfolios/krw-btc/comments/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ - -H "Authorization: Bearer " -``` - -Response 204: - -- No content. - -## 5. Strategy Sets - -### 5.1 GET /strategy-sets - -## 6. Auth - -### 6.1 POST /auth/login - -Request: - -```bash -curl -X POST "http://localhost:8000/auth/login" \ - -H "Content-Type: application/json" \ - -d '{ - "username": "admin", - "password": "bootstrap123" - }' -``` - -Response 200: - -```json -{ - "access_token": "plain-token-value", - "token_type": "bearer", - "expires_at": "2026-04-03T12:00:00Z", - "permission": "admin", - "user": { - "id": "user-1", - "username": "admin", - "email": "admin@example.com", - "permission": "admin", - "is_active": true, - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T00:00:00Z", - "last_login_at": "2026-04-03T00:00:00Z" - } -} -``` - -### 6.2 POST /auth/tokens - -Request: - -```bash -curl -X POST "http://localhost:8000/auth/tokens" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "name": "mobile-app", - "permission": "read", - "expires_in_days": 30 - }' -``` - -Response 201: - -```json -{ - "token": "plain-client-token", - "token_type": "bearer", - "auth_token": { - "id": "token-1", - "user_id": "user-1", - "name": "mobile-app", - "token_prefix": "abcd1234", - "permission": "read", - "token_type": "client", - "bot_principal_id": null, - "expires_at": "2026-05-03T00:00:00Z", - "last_used_at": null, - "revoked_at": null, - "created_at": "2026-04-03T00:00:00Z" - } -} -``` - -### 6.3 GET /auth/tokens - -Request: - -```bash -curl -X GET "http://localhost:8000/auth/tokens" \ - -H "Authorization: Bearer " -``` - -### 6.4 DELETE /auth/tokens/{token_id} - -Request: - -```bash -curl -X DELETE "http://localhost:8000/auth/tokens/token-1" \ - -H "Authorization: Bearer " -``` - -Response 204: - -- No content. - -### 6.5 POST /auth/bots - -Request: - -```bash -curl -X POST "http://localhost:8000/auth/bots" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "name": "market-bot", - "permission": "trade-write", - "description": "External trading bot" - }' -``` - -Response 201: - -```json -{ - "id": "bot-1", - "name": "market-bot", - "permission": "trade-write", - "description": "External trading bot", - "is_active": true, - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T00:00:00Z", - "deactivated_at": null -} -``` - -### 6.6 GET /auth/bots - -Request: - -```bash -curl -X GET "http://localhost:8000/auth/bots?is_active=true" \ - -H "Authorization: Bearer " -``` - -### 6.7 POST /auth/bots/{bot_id}/tokens - -Request: - -```bash -curl -X POST "http://localhost:8000/auth/bots/bot-1/tokens" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "name": "prod-key", - "permission": "read", - "expires_in_days": 30 - }' -``` - -Response 201: - -```json -{ - "token": "plain-bot-token", - "token_type": "bearer", - "auth_token": { - "id": "token-2", - "user_id": "user-1", - "name": "prod-key", - "token_prefix": "efgh5678", - "permission": "read", - "token_type": "bot", - "bot_principal_id": "bot-1", - "expires_at": "2026-05-03T00:00:00Z", - "last_used_at": null, - "revoked_at": null, - "created_at": "2026-04-03T00:00:00Z" - } -} -``` - -### 6.8 GET /auth/bots/{bot_id}/tokens - -Request: - -```bash -curl -X GET "http://localhost:8000/auth/bots/bot-1/tokens" \ - -H "Authorization: Bearer " -``` - -### 6.9 DELETE /auth/bots/{bot_id}/tokens/{token_id} - -Request: - -```bash -curl -X DELETE "http://localhost:8000/auth/bots/bot-1/tokens/token-2" \ - -H "Authorization: Bearer " -``` - -Response 204: - -- No content. - -### 6.10 DELETE /auth/bots/{bot_id} - -Request: - -```bash -curl -X DELETE "http://localhost:8000/auth/bots/bot-1" \ - -H "Authorization: Bearer " -``` - -Response 200: - -```json -{ - "id": "bot-1", - "name": "market-bot", - "permission": "trade-write", - "description": "External trading bot", - "is_active": false, - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T01:00:00Z", - "deactivated_at": "2026-04-03T01:00:00Z" -} -``` - -## 7. Users - -All user management endpoints require an admin token. - -### 7.1 GET /users - -Request: - -```bash -curl -X GET "http://localhost:8000/users" \ - -H "Authorization: Bearer " -``` - -Response 200: - -```json -[ - { - "id": "user-1", - "username": "alice", - "email": "alice@example.com", - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T00:00:00Z" - } -] -``` - -### 7.2 GET /users/{user_id} - -Request: - -```bash -curl -X GET "http://localhost:8000/users/user-1" \ - -H "Authorization: Bearer " -``` - -Response 200: - -```json -{ - "id": "user-1", - "username": "alice", - "email": "alice@example.com", - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T00:00:00Z" -} -``` - -### 7.3 POST /users - -Request: - -```bash -curl -X POST "http://localhost:8000/users" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "username": "alice", - "email": "alice@example.com", - "password": "password123", - "permission": "read", - "is_active": true - }' -``` - -Response 201: - -```json -{ - "id": "user-1", - "username": "alice", - "email": "alice@example.com", - "permission": "read", - "is_active": true, - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T00:00:00Z", - "last_login_at": null -} -``` - -### 7.4 PATCH /users/{user_id} - -Request: - -```bash -curl -X PATCH "http://localhost:8000/users/user-1" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "username": "alice-updated" - }' -``` - -Response 200: - -```json -{ - "id": "user-1", - "username": "alice-updated", - "email": "alice@example.com", - "permission": "read", - "is_active": true, - "created_at": "2026-04-03T00:00:00Z", - "updated_at": "2026-04-03T00:10:00Z", - "last_login_at": null -} -``` - -### 7.5 DELETE /users/{user_id} - -Request: - -```bash -curl -X DELETE "http://localhost:8000/users/user-1" \ - -H "Authorization: Bearer " -``` - -Response 204: - -- No content. - -Request: - -```bash -curl -X GET "http://localhost:8000/strategy-sets" \ - -H "Authorization: Bearer " -``` - -Response 200: - -```json -[ - { - "id": "set-1", - "name": "target_value", - "config": "{\"target\": 1000}", - "nick": "target_value:abc123def456", - "created_at": "2026-04-03T08:13:08.318Z" - }, - { - "id": "set-2", - "name": "momentum", - "config": "{\"window\": 20}", - "nick": "momentum:xyz789uvw012", - "created_at": "2026-04-03T08:14:15.450Z" - } -] -``` - -### 5.2 POST /strategy-sets - -Request: - -```bash -curl -X POST "http://localhost:8000/strategy-sets" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "name": "target_value", - "config": "{\"target\": 1000}" - }' -``` - -Response 201: - -```json -{ - "id": "set-1", - "name": "target_value", - "config": "{\"target\": 1000}", - "nick": "target_value:abc123def456", - "created_at": "2026-04-03T08:13:08.318Z" -} -``` - -### 5.3 PATCH /strategy-sets/{strategy_set_id} - -Request: - -```bash -curl -X PATCH "http://localhost:8000/strategy-sets/set-1" \ - -H "Authorization: Bearer " \ - -H "Content-Type: application/json" \ - -d '{ - "config": "{\"target\": 2000}" - }' -``` - -Response 200: - -```json -{ - "id": "set-1-updated", - "name": "target_value", - "config": "{\"target\": 2000}", - "nick": "target_value:def789ghi012", - "created_at": "2026-04-03T08:13:08.318Z" -} -``` - -Note: Since strategy sets are identified by (name, config) uniqueness, updating may result in a new strategy set ID. - -### 5.4 DELETE /strategy-sets/{strategy_set_id} - -Request: - -```bash -curl -X DELETE "http://localhost:8000/strategy-sets/set-1" \ - -H "Authorization: Bearer " -``` - -Response 204: - -- No content on success. - -Response 409 Conflict (if strategy set is in use): - -```json -{ - "detail": "Strategy set is in use by 1 portfolio(s) and cannot be deleted" -} -``` - -## 6. Common Error Examples - -### 6.1 Missing token - -Response 401: - -```json -{ - "detail": "Missing bearer token" -} -``` - -### 6.2 Invalid token - -Response 401: - -```json -{ - "detail": "Invalid token" -} -``` - -### 6.3 Insufficient permission - -Response 403: - -```json -{ - "detail": "Insufficient permission" -} -``` diff --git a/docs/db-schema-spec.md b/docs/db-schema-spec.md deleted file mode 100644 index 0f1f9ed..0000000 --- a/docs/db-schema-spec.md +++ /dev/null @@ -1,332 +0,0 @@ -# PortfolioHub DB Schema Specification - -## 1. Purpose - -This document defines the database schema in an AI-agent-friendly format. - -- Source of truth: Alembic revisions in `migrations/versions` -- Current baseline revision: `20260410_0009` -- Baseline files: - - `migrations/versions/20260403_0001_initial_portfolio_schema.py` - - `migrations/versions/20260403_0002_strategy_set_columns.py` - - `migrations/versions/20260403_0003_portfolio_strategy_refs.py` - - `migrations/versions/20260403_0004_users_table.py` - - `migrations/versions/20260403_0005_auth_user_fields_and_tokens.py` - - `migrations/versions/20260403_0006_bot_principals_and_bot_tokens.py` - - `migrations/versions/20260403_0007_bot_principal_permission_only.py` - - `migrations/versions/20260403_0008_portfolio_name_column.py` - - `migrations/versions/20260410_0009_portfolio_comments_table.py` - -## 2. Scope - -Schema objects covered by baseline revision: - -- `strategy_sets` -- `users` -- `auth_tokens` -- `bot_principals` -- `portfolios` -- `portfolio_ledger` -- `portfolio_comments` -- secondary indexes -- Timescale hypertable conversion rule for `portfolio_ledger` - -## 3. Conventions - -- Data type names follow SQLAlchemy/Alembic declaration. -- `NULL` means nullable. -- Default means DB-level `server_default` where defined. -- Relationship columns are documented even when no FK constraint exists. - -## 4. Tables - -### 4.1 users - -Purpose: - -- Stores admin-managed user records for operational ownership and future identity mapping. - -Columns: - -| Column | Type | Null | Default | Constraints / Notes | -|---|---|---|---|---| -| id | Text | NO | - | Primary Key | -| username | Text | NO | - | Unique username | -| email | Text | YES | - | Unique when present | -| permission | Text | NO | "read" | User permission level | -| is_active | Boolean | NO | TRUE | Whether login is allowed | -| password_hash | Text | YES | - | Scrypt password hash | -| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | -| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp | -| last_login_at | DateTime(timezone=True) | YES | - | Last successful login timestamp | - -Constraints: - -- Primary key: `id` -- Unique: `uq_users_username` on (`username`) -- Unique: `uq_users_email` on (`email`) - -### 4.2 auth_tokens - -Purpose: - -- Stores DB-issued session tokens and client tokens. - -Columns: - -| Column | Type | Null | Default | Constraints / Notes | -|---|---|---|---|---| -| id | Text | NO | - | Primary Key | -| user_id | Text | NO | - | Logical user reference | -| name | Text | NO | - | Human-readable token name | -| token_prefix | Text | NO | - | Display prefix for issued token | -| token_hash | Text | NO | - | Unique SHA-256 token hash | -| permission | Text | NO | - | Effective token permission | -| token_type | Text | NO | - | `session`, `client`, or `bot` | -| bot_principal_id | Text | YES | - | Logical reference to `bot_principals.id` | -| expires_at | DateTime(timezone=True) | YES | - | Expiration time | -| last_used_at | DateTime(timezone=True) | YES | - | Last successful use | -| revoked_at | DateTime(timezone=True) | YES | - | Revocation time | -| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | - -Constraints: - -- Primary key: `id` -- Unique: `uq_auth_tokens_token_hash` on (`token_hash`) - -### 4.3 bot_principals - -Purpose: - -- Stores external bot identities for admin-managed API key issuance and lifecycle control. - -Columns: - -| Column | Type | Null | Default | Constraints / Notes | -|---|---|---|---|---| -| id | Text | NO | - | Primary Key | -| name | Text | NO | - | Unique bot name | -| permission | Text | NO | - | Bot permission ceiling | -| description | Text | YES | - | Optional operator description | -| is_active | Boolean | NO | TRUE | When false, bot tokens are rejected | -| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | -| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp | -| deactivated_at | DateTime(timezone=True) | YES | - | Deactivation time | - -Constraints: - -- Primary key: `id` -- Unique: `uq_bot_principals_name` on (`name`) - -### 4.3 strategy_sets - -Purpose: - -- Stores normalized strategy configuration snapshots. - -Columns: - -| Column | Type | Null | Default | Constraints / Notes | -|---|---|---|---|---| -| id | Text | NO | - | Primary Key | -| name | Text | NO | - | Strategy name, part of unique pair | -| config | Text | NO | "" | Strategy config payload, part of unique pair | -| nick | Text | NO | - | Unique strategy set nickname | -| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | - -Constraints: - -- Primary key: `id` -- Unique: `uq_strategy_sets_name_config` on (`name`, `config`) -- Unique: `uq_strategy_sets_nick` on (`nick`) - -### 4.4 portfolios - -Purpose: - -- Stores current portfolio state and strategy linkage. - -Current baseline note: - -- The columns below describe the currently migrated schema through revision `20260403_0008`. - -Columns: - -| Column | Type | Null | Default | Constraints / Notes | -|---|---|---|---|---| -| portfolio_id | Text | NO | - | Primary Key | -| broker | Text | NO | "kis" | Broker code | -| status | Text | NO | "enabled" | Portfolio status | -| name | Text | NO | - | Asset name | -| symbol | Text | NO | - | Trading symbol | -| cash_balance | Float | NO | - | Current cash balance | -| asset_quantity | Float | NO | - | Current asset quantity | -| asset_price | Float | NO | 0 | Current price snapshot | -| profit_buffer | Float | NO | 0 | Profit buffer amount | -| active_strategy_id | Text | YES | - | Logical reference to `strategy_sets.id` | -| avabile_strategies | Text[] | YES | - | Nullable array of `strategy_sets.id` values | -| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | -| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp | -| last_trade_at | DateTime(timezone=True) | YES | - | Last trade time | - -Constraints: - -- Primary key: `portfolio_id` -- Foreign key constraints: none in baseline - -Constraint notes: - -- `active_strategy_id` is a nullable logical reference to `strategy_sets.id` -- `avabile_strategies` stores zero or more selectable strategy set IDs when present - -### 4.5 portfolio_ledger - -Purpose: - -- Stores append-like ledger snapshots per portfolio action. - -Columns: - -| Column | Type | Null | Default | Constraints / Notes | -|---|---|---|---|---| -| portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` (no FK) | -| recorded_at | DateTime(timezone=True) | NO | - | Event timestamp | -| action | Text | NO | - | Action type | -| price | Float | NO | - | Trade/valuation unit price | -| quantity | Float | NO | - | Quantity delta | -| fee_amount | Float | NO | 0 | Fees | -| tax_amount | Float | NO | 0 | Taxes | -| cash_balance_after | Float | NO | - | Post-action cash | -| asset_quantity_after | Float | NO | - | Post-action asset quantity | -| total_value_after | Float | NO | - | Post-action total value | -| profit_buffer_after | Float | NO | 0 | Post-action profit buffer | -| strategy_set_id | Text | NO | "legacy" | Logical strategy set reference | -| note | Text | NO | "" | Operator/system note | - -Constraints: - -- Primary key: none in baseline -- Foreign key constraints: none in baseline - -### 4.6 portfolio_comments - -Purpose: - -- Stores user-authored comment history per portfolio for memo/annotation tracking. -- Supports multiline text content. -- Comments are isolated per portfolio and listed in reverse chronological order. - -Columns: - -| Column | Type | Null | Default | Constraints / Notes | -|---|---|---|---|---| -| id | Text | NO | - | Primary Key (UUID) | -| portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` (no FK) | -| content | Text | NO | - | Comment text; supports multiline | -| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | -| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp | - -Constraints: - -- Primary key: `id` -- Foreign key constraints: none - -## 5. Indexes - -### 5.1 ix_users_created_at - -- Table: `users` -- Columns/expressions: (`created_at`) -- Unique: no -- Intent: speed operational listing by recency. - -### 5.2 idx_auth_tokens_user_created - -- Table: `auth_tokens` -- Columns/expressions: (`user_id`, `created_at`) -- Unique: no -- Intent: fetch current user's issued tokens efficiently. - -### 5.3 idx_auth_tokens_hash_active - -- Table: `auth_tokens` -- Columns/expressions: (`token_hash`) -- Unique: no -- Intent: resolve bearer tokens efficiently. - -### 5.4 idx_auth_tokens_bot_created - -- Table: `auth_tokens` -- Columns/expressions: (`bot_principal_id`, `created_at`) -- Unique: no -- Intent: fetch bot-issued token lists efficiently. - -### 5.5 idx_bot_principals_active_created - -- Table: `bot_principals` -- Columns/expressions: (`is_active`, `created_at`) -- Unique: no -- Intent: speed operational filtering of active/inactive bots. - -### 5.6 idx_portfolio_ledger_portfolio_time - -- Table: `portfolio_ledger` -- Columns/expressions: (`portfolio_id`, `recorded_at DESC`) -- Unique: no -- Intent: fetch recent ledger entries per portfolio efficiently. - -### 5.7 idx_portfolios_broker_status_updated - -- Table: `portfolios` -- Columns/expressions: (`broker`, `status`, `updated_at DESC`) -- Unique: no -- Intent: speed list/filter queries by broker/status and recency. - -### 5.8 idx_portfolio_comments_portfolio_created - -- Table: `portfolio_comments` -- Columns/expressions: (`portfolio_id`, `created_at DESC`) -- Unique: no -- Intent: fetch comments per portfolio efficiently in reverse chronological order. - -Conditional behavior in migration upgrade: - -- Checks whether extension `timescaledb` exists. -- If present, executes: - -`create_hypertable('portfolio_ledger', 'recorded_at', if_not_exists => TRUE)` - -Implication: - -- `portfolio_ledger` acts as a hypertable in environments with TimescaleDB extension enabled. - -## 7. Integrity Notes - -Current baseline intentionally has no DB-level FK constraints. - -- Logical relations are managed by application/repository logic. -- When adding FK constraints in future migrations, update this document and include migration IDs. -- Cascade behavior implemented in application layer: - - Deleting a portfolio also deletes all associated `portfolio_comments` rows (hard delete, no FK constraint). - -## 8. Change Management Rule - -When schema changes: - -1. Add/modify Alembic revision under `migrations/versions`. -2. Update this document in the same commit. -3. Keep revision mapping table below in sync. - -## 9. Revision Mapping - -| Revision ID | File | Summary | -|---|---|---| -| 20260403_0001 | migrations/versions/20260403_0001_initial_portfolio_schema.py | Initial schema for strategy sets, portfolios, ledger, indexes, optional hypertable | -| 20260403_0002 | migrations/versions/20260403_0002_strategy_set_columns.py | Rename `strategy_name`->`name`, `strategy_config`->`config`, add unique `nick`, handle legacy unnamed unique constraint | -| 20260403_0003 | migrations/versions/20260403_0003_portfolio_strategy_refs.py | Remove embedded portfolio strategy fields, add `active_strategy_id` and `avabile_strategies` | -| 20260403_0004 | migrations/versions/20260403_0004_users_table.py | Add `users` table with unique username/email and created_at index | -| 20260403_0005 | migrations/versions/20260403_0005_auth_user_fields_and_tokens.py | Add password/permission/login fields to `users` and create `auth_tokens` table | -| 20260403_0006 | migrations/versions/20260403_0006_bot_principals_and_bot_tokens.py | Add `bot_principals` table and `auth_tokens.bot_principal_id` for bot API key management | -| 20260403_0007 | migrations/versions/20260403_0007_bot_principal_permission_only.py | Remove `bot_principals.owner_user_id` and enforce bot-local `permission` model | -| 20260403_0008 | migrations/versions/20260403_0008_portfolio_name_column.py | Add non-null `portfolios.name` (asset display name), backfilled from `symbol` | -| 20260410_0009 | migrations/versions/20260410_0009_portfolio_comments_table.py | Add `portfolio_comments` table for per-portfolio memo/comment history | diff --git a/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index 0c5f3c0..0000000 --- a/docs/deployment.md +++ /dev/null @@ -1,194 +0,0 @@ -# Production Deployment: HTTPS with Caddy - -This guide describes how to deploy PortfolioHub behind [Caddy](https://caddyserver.com/), a reverse proxy that automatically provisions and renews TLS certificates from Let's Encrypt or ZeroSSL. - -## Prerequisites - -- A Linux/Ubuntu server with a public IP address -- A domain name (e.g., `api.example.com`) with a DNS `A` record pointing to that IP -- Ports **80** and **443** open on your firewall (see [Firewall Setup](#firewall-setup)) - ---- - -## 1. Install Caddy (Ubuntu/Debian) - -```bash -sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl -curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ - | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg -curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ - | sudo tee /etc/apt/sources.list.d/caddy-stable.list -sudo apt update -sudo apt install caddy -``` - -Verify the installation: - -```bash -caddy version -``` - ---- - -## 2. Configure the Caddyfile - -Create or edit `/etc/caddy/Caddyfile`: - -``` -your-domain.com { - reverse_proxy localhost:8000 -} -``` - -Replace `your-domain.com` with your actual domain. Caddy automatically: - -- Obtains and renews TLS certificates from Let's Encrypt/ZeroSSL -- Redirects plain HTTP (port 80) to HTTPS (port 443) -- Forwards all requests to the PortfolioHub `uvicorn` process on port 8000 - -Reload Caddy after editing the Caddyfile: - -```bash -sudo systemctl reload caddy -``` - ---- - -## 3. CORS Configuration - -When PortfolioHub is accessed from a browser-based frontend over HTTPS, the `PORTFOLIOHUB_ALLOWED_ORIGINS` environment variable must list the exact HTTPS origin(s) of your frontend. - -Set the variable in your environment or `.env` file: - -```bash -PORTFOLIOHUB_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com -``` - -Multiple origins are comma-separated. For the CORS policy details see section 12 of `docs/portfoliohub-spec.md`. - ---- - -## 4. Start PortfolioHub for Production - -Run `uvicorn` without `--reload` and bind only to `localhost` so that external traffic is always routed through Caddy: - -```bash -uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2 -``` - -For a systemd service, create `/etc/systemd/system/portfoliohub.service`: - -```ini -[Unit] -Description=PortfolioHub API -After=network.target - -[Service] -User=www-data -WorkingDirectory=/opt/portfoliohub -EnvironmentFile=/opt/portfoliohub/.env -ExecStart=/opt/portfoliohub/.venv/bin/uvicorn app.main:app \ - --host 127.0.0.1 --port 8000 --workers 2 -Restart=on-failure - -[Install] -WantedBy=multi-user.target -``` - -Enable and start the service: - -```bash -sudo systemctl daemon-reload -sudo systemctl enable portfoliohub -sudo systemctl start portfoliohub -``` - ---- - -## 5. Docker Compose Setup (Optional) - -If you run PortfolioHub in Docker, you can add Caddy as a sidecar container. - -`docker-compose.yml`: - -```yaml -services: - app: - build: . - expose: - - "8000" - environment: - - PORTFOLIOHUB_ALLOWED_ORIGINS=https://your-domain.com - # add other env vars or reference an env_file - restart: unless-stopped - - caddy: - image: caddy:2-alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./Caddyfile:/etc/caddy/Caddyfile:ro - - caddy_data:/data - - caddy_config:/config - depends_on: - - app - restart: unless-stopped - -volumes: - caddy_data: - caddy_config: -``` - -`Caddyfile` (placed next to `docker-compose.yml`): - -``` -your-domain.com { - reverse_proxy app:8000 -} -``` - -Note: inside a Docker Compose network, Caddy reaches the app container via its service name (`app`) instead of `localhost`. - -Start everything: - -```bash -docker compose up -d -``` - ---- - -## 6. Firewall Setup - -Open ports 80 (HTTP, needed for ACME certificate challenge) and 443 (HTTPS): - -```bash -# UFW (Ubuntu) -sudo ufw allow 80/tcp -sudo ufw allow 443/tcp -sudo ufw reload -``` - -```bash -# firewalld (RHEL/CentOS/Fedora) -sudo firewall-cmd --permanent --add-service=http -sudo firewall-cmd --permanent --add-service=https -sudo firewall-cmd --reload -``` - ---- - -## 7. Verify the Deployment - -```bash -# Health check over HTTPS -curl https://your-domain.com/health -# Expected: {"status":"ok"} -``` - -Check Caddy's status and certificate renewal: - -```bash -sudo systemctl status caddy -sudo journalctl -u caddy -f -``` diff --git a/docs/portfoliohub-spec.md b/docs/portfoliohub-spec.md deleted file mode 100644 index 3a1fb6a..0000000 --- a/docs/portfoliohub-spec.md +++ /dev/null @@ -1,697 +0,0 @@ -# PortfolioHub Specification - -## 1. Overview - -PortfolioHub is a FastAPI-based portfolio lifecycle service with a Typer CLI. - -- Runtime: Python 3.11+ -- API framework: FastAPI -- CLI framework: Typer -- DB driver: Psycopg v3 -- Migration: Alembic - -Primary goals: - -- Manage portfolio lifecycle (create/update/delete/reset) -- Record trade and balance-affecting actions (buy/sell/invest/transfer) -- Expose status and ledger snapshots through API and CLI -- Keep API and CLI behavior aligned through a shared service/repository layer - -## 2. Scope and Non-Goals - -In scope: - -- Authenticated portfolio read and write operations -- Admin-managed user CRUD operations -- Role-based access control (read, trade-write, admin) -- Optional PostgreSQL/Timescale-backed repository via feature flag -- Operational commands for DB bootstrap and migration - -Current non-goals / limitations: - -- In-memory fallback repository is not implemented for production usage -- External broker execution is not handled here (service focuses on portfolio state) -- Advanced observability wiring for Redis/Loki is not yet implemented in runtime paths - -## 3. Architecture - -Layered components: - -- API layer: HTTP routes and request validation -- CLI layer: command parsing and output rendering -- Service layer: orchestration and response shaping -- Domain layer: business logic rules -- Repository layer: persistence abstraction and concrete Postgres implementation - -Key design principle: - -- API and CLI call the same `PortfolioService`, so behavior is consistent regardless of entrypoint. - -## 4. Entrypoints - -HTTP service: - -- App factory: `app.main:create_app` -- ASGI app: `app.main:app` -- OpenAPI docs: `/docs` -- ReDoc: `/redoc` - -CLI: - -- Console script: `portfoliohub` -- Entry function: `app.cli.main:main` - -## 5. Configuration Specification - -Configuration precedence (high to low): - -1. Environment variables -2. `.env` file (loaded at startup if present) -3. Code defaults - -Core settings: - -- `PORTFOLIOHUB_ALLOWED_ORIGINS`: comma-separated CORS origins -- `PORTFOLIOHUB_BOOTSTRAP_USERNAME`: initial username created on startup when users table is empty -- `PORTFOLIOHUB_BOOTSTRAP_EMAIL`: optional email for the bootstrap user -- `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`: initial password for the bootstrap admin user -- `PORTFOLIOHUB_READ_TOKENS`: comma-separated read tokens -- `PORTFOLIOHUB_TRADE_WRITE_TOKENS`: comma-separated write tokens -- `PORTFOLIOHUB_ADMIN_TOKENS`: comma-separated admin tokens -- `PORTFOLIOHUB_DB_ENABLED`: enables DB repository selection when `true` -- `PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS`: login session token TTL in hours -- `PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS`: default client token TTL in days -- `PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS`: default bot token TTL in days - -Database settings (active source): - -- `TIMESCALEDB_HOST` -- `TIMESCALEDB_PORT` -- `TIMESCALEDB_NAME` -- `TIMESCALEDB_USER` -- `TIMESCALEDB_PASSWORD` -- `TIMESCALEDB_SSLMODE` -- `TIMESCALEDB_CONNECT_TIMEOUT_SEC` -- `TIMESCALEDB_APPLICATION_NAME` - -Derived setting: - -- `active_database_url` uses PostgreSQL psycopg URL format with encoded credentials and query options. - -## 6. Authentication and Authorization - -Auth scheme: - -- `Authorization: Bearer ` required for protected API routes - -Permission levels: - -- `read` -- `trade-write` -- `admin` - -Authorization hierarchy: - -- `admin` can access all endpoints -- `trade-write` can access `trade-write` + `read` -- `read` can access only read endpoints - -Failure responses: - -- Missing/invalid bearer header: `401` -- Unknown token: `401` -- Insufficient permission: `403` - -## 7. HTTP API Specification - -Base routes: - -- Health: `/health` -- Auth: `/auth` -- Portfolio: `/portfolios` -- Users: `/users` - -### 7.1 Health Endpoint - -- `GET /health` -- Auth: none -- Response `200`: - -```json -{"status": "ok"} -``` - -### 7.2 Portfolio Endpoints - -#### List Portfolios - -- `GET /portfolios` -- Auth: `read` -- Query params: - - `broker` (optional string) - - `status_filter` (optional string) -- Response `200`: array of `PortfolioListItem` - -#### Get Portfolio Status - -- `GET /portfolios/{portfolio_id}` -- Auth: `read` -- Query params: - - `recent_trades` (int, default `5`) -- Response `200`: `PortfolioDetailResponse` - -#### Create Portfolio - -- `POST /portfolios` -- Auth: `trade-write` -- Body: `CreatePortfolioRequest` -- Response `201`: `PortfolioDetailResponse` - -#### Update Portfolio - -- `PATCH /portfolios/{portfolio_id}` -- Auth: `trade-write` -- Body: `UpdatePortfolioRequest` -- Response `200`: `PortfolioDetailResponse` - -#### Buy - -- `POST /portfolios/{portfolio_id}/buy` -- Auth: `trade-write` -- Body: `BuyRequest` -- Response `200`: `PortfolioDetailResponse` - -#### Sell - -- `POST /portfolios/{portfolio_id}/sell` -- Auth: `trade-write` -- Body: `SellRequest` -- Response `200`: `PortfolioDetailResponse` - -#### Invest - -- `POST /portfolios/{portfolio_id}/invest` -- Auth: `trade-write` -- Body: `InvestRequest` -- Response `200`: `PortfolioDetailResponse` - -#### Transfer - -- `POST /portfolios/{portfolio_id}/transfer` -- Auth: `trade-write` -- Body: `TransferRequest` -- Response `200`: `PortfolioDetailResponse` - -#### Reset - -- `POST /portfolios/{portfolio_id}/reset` -- Auth: `trade-write` -- Body: `ResetPortfolioRequest` -- Response `200`: `PortfolioDetailResponse` - -#### Delete - -- `DELETE /portfolios/{portfolio_id}` -- Auth: `admin` -- Query params: - - `keep_ledger` (bool, default `false`) -- Response `204`: no content - -#### Create Comment - -- `POST /portfolios/{portfolio_id}/comments` -- Auth: `trade-write` -- Body: `CommentCreateRequest` (`content: str`) -- Response `201`: `CommentResponse` -- Response `404`: portfolio not found - -#### List Comments - -- `GET /portfolios/{portfolio_id}/comments` -- Auth: `read` -- Response `200`: array of `CommentResponse`, sorted by `created_at DESC` -- Response `404`: portfolio not found - -#### Update Comment - -- `PATCH /portfolios/{portfolio_id}/comments/{comment_id}` -- Auth: `trade-write` -- Body: `CommentUpdateRequest` (`content: str`) -- Response `200`: `CommentResponse` -- Response `404`: portfolio or comment not found - -#### Delete Comment - -- `DELETE /portfolios/{portfolio_id}/comments/{comment_id}` -- Auth: `trade-write` -- Response `204`: no content -- Response `404`: portfolio or comment not found - -### 7.3 Strategy Set Endpoints - -#### List Strategy Sets - -- `GET /strategy-sets` -- Auth: `read` -- Response `200`: array of `StrategySetResponse` - -#### Create Strategy Set - -- `POST /strategy-sets` -- Auth: `trade-write` -- Body: `CreateStrategySetRequest` -- Response `201`: `StrategySetResponse` - -Note: Uses `get_or_create_strategy_set` logic, so if (name, config) pair already exists, returns existing strategy set. - -#### Update Strategy Set - -- `PATCH /strategy-sets/{strategy_set_id}` -- Auth: `trade-write` -- Body: `UpdateStrategySetRequest` -- Response `200`: `StrategySetResponse` - -Note: Since strategy sets are identified by (name, config) uniqueness, updating may result in a new strategy set ID if name or config changes. - -#### Delete Strategy Set - -- `DELETE /strategy-sets/{strategy_set_id}` -- Auth: `trade-write` -- Response `204`: no content -- Response `409`: Conflict if strategy set is in use by any portfolio - -### 7.4 Example Requests and Responses - -- See `docs/api-examples.md` for maintained request/response examples. - -### 7.5 Auth Endpoints - -#### Login - -- `POST /auth/login` -- Auth: none -- Body: `LoginRequest` -- Response `200`: `TokenResponse` -- Response `401`: invalid username/password - -#### List Current Tokens - -- `GET /auth/tokens` -- Auth: `read` or higher with DB-issued token/session -- Response `200`: array of `AuthTokenResponse` - -#### Create Client Token - -- `POST /auth/tokens` -- Auth: `read` or higher with DB-issued token/session -- Body: `CreateClientTokenRequest` -- Response `201`: `IssuedClientTokenResponse` -- Response `403`: requested token permission exceeds current permission - -#### Revoke Client Token - -- `DELETE /auth/tokens/{token_id}` -- Auth: `read` or higher with DB-issued token/session -- Response `204`: no content -- Response `404`: token not found - -#### Create Bot Principal - -- `POST /auth/bots` -- Auth: `admin` -- Body: `CreateBotPrincipalRequest` -- Response `201`: `BotPrincipalResponse` - -#### List Bot Principals - -- `GET /auth/bots` -- Auth: `admin` -- Query params: - - `is_active` (optional bool) -- Response `200`: array of `BotPrincipalResponse` - -#### Get Bot Principal - -- `GET /auth/bots/{bot_id}` -- Auth: `admin` -- Response `200`: `BotPrincipalResponse` -- Response `404`: bot not found - -#### Deactivate Bot Principal - -- `DELETE /auth/bots/{bot_id}` -- Auth: `admin` -- Response `200`: `BotPrincipalResponse` -- Behavior: deactivates bot and revokes all active tokens linked to the bot - -#### Issue Bot Token - -- `POST /auth/bots/{bot_id}/tokens` -- Auth: `admin` -- Body: `CreateBotTokenRequest` -- Response `201`: `IssuedBotTokenResponse` - -#### List Bot Tokens - -- `GET /auth/bots/{bot_id}/tokens` -- Auth: `admin` -- Response `200`: array of `AuthTokenResponse` - -#### Revoke Bot Token - -- `DELETE /auth/bots/{bot_id}/tokens/{token_id}` -- Auth: `admin` -- Response `204`: no content - -### 7.6 User Endpoints - -All user management endpoints are admin-only. - -#### List Users - -- `GET /users` -- Auth: `admin` -- Response `200`: array of `UserResponse` - -#### Get User - -- `GET /users/{user_id}` -- Auth: `admin` -- Response `200`: `UserResponse` -- Response `404`: user not found - -#### Create User - -- `POST /users` -- Auth: `admin` -- Body: `CreateUserRequest` -- Response `201`: `UserResponse` -- Response `409`: duplicate username or email - -Bootstrap rule: - -- If DB-backed persistence is enabled and both `PORTFOLIOHUB_BOOTSTRAP_USERNAME` and `PORTFOLIOHUB_BOOTSTRAP_PASSWORD` are configured, application startup creates the bootstrap admin user only when the `users` table is empty. - -#### Update User - -- `PATCH /users/{user_id}` -- Auth: `admin` -- Body: `UpdateUserRequest` -- Response `200`: `UserResponse` -- Response `404`: user not found -- Response `409`: duplicate username or email - -#### Delete User - -- `DELETE /users/{user_id}` -- Auth: `admin` -- Response `204`: no content -- Response `404`: user not found - -## 8. Schema Specification - -### 8.1 Request Models - -`CreatePortfolioRequest` - -- `portfolio_id: str` -- `broker: str = "kis"` -- `status: str = "enabled"` -- `name: str | null` (when null, defaults to `symbol`) -- `symbol: str` -- `strategy_name: str = "target_value"` -- `strategy_params: object = {}` -- `active_strategy_id: str | null` -- `avabile_strategies: string[] | null` -- `cash_balance: float >= 0` -- `asset_quantity: float >= 0` -- `asset_price: float >= 0` -- `profit_buffer: float >= 0` - -`UpdatePortfolioRequest` (all fields optional) - -- `broker: str | null` -- `status: str | null` -- `name: str | null` -- `symbol: str | null` -- `strategy_name: str | null` -- `strategy_params: object | null` -- `active_strategy_id: str | null` -- `avabile_strategies: string[] | null` -- `asset_price: float >= 0 | null` - -`BuyRequest` / `SellRequest` - -- `quantity: float > 0` - -`CreateUserRequest` - -- `username: str` -- `email: str | null` -- `password: str` -- `permission: "read" | "trade-write" | "admin"` -- `is_active: bool` - -`UpdateUserRequest` - -- `username: str | null` -- `email: str | null` -- `password: str | null` -- `permission: "read" | "trade-write" | "admin" | null` -- `is_active: bool | null` - -`LoginRequest` - -- `username: str` -- `password: str` - -`CreateClientTokenRequest` - -- `name: str` -- `permission: "read" | "trade-write" | "admin" | null` -- `expires_in_days: int | null` - -`CreateBotPrincipalRequest` - -- `name: str` -- `permission: "read" | "trade-write" | "admin"` -- `description: str | null` - -`CreateBotTokenRequest` - -- `name: str` -- `permission: "read" | "trade-write" | "admin" | null` -- `expires_in_days: int | null` -- `price: float > 0` -- `fee: float >= 0` -- `tax: float >= 0` -- `note: str = ""` - -`InvestRequest` - -- `movement_type: "deposit" | "profit" | "withdraw"` -- `amount: float > 0` -- `note: str = ""` - -`TransferRequest` - -- `direction: "to-buffer" | "from-buffer"` -- `amount: float > 0` -- `note: str = ""` - -`ResetPortfolioRequest` - -- `infer_symbol: str | null` - -`CommentCreateRequest` - -- `content: str` - -`CommentUpdateRequest` - -- `content: str` - -`CreateStrategySetRequest` - -- `name: str` -- `config: str` - -`UpdateStrategySetRequest` (all fields optional) - -- `name: str | null` -- `config: str | null` - -### 8.2 Response Models - -`PortfolioListItem` - -- Includes portfolio summary fields and computed `total_value` - -`PortfolioSummary` - -- `portfolio_id: str` -- `broker: str` -- `status: str` -- `name: str` -- `symbol: str` -- `cash_balance: float` -- `asset_quantity: float` -- `price: float` -- `profit_buffer: float` -- `active_strategy_id: str | null` -- `avabile_strategies: string[] | null` -- `created_at: datetime | null` -- `updated_at: datetime | null` -- `last_trade_at: datetime | null` - -`PortfolioDetailResponse` - -- `portfolio: PortfolioSummary` -- `recent_entries: LedgerEntryResponse[]` -- `strategy_set: StrategySetResponse | null` -- `live_quantity: float | null` - -`StrategySetResponse` - -- `id: str` -- `name: str` -- `config: str` -- `nick: str` -- `created_at: datetime | null` - -`LedgerEntryResponse` - -- Snapshot-oriented ledger response including balances after each action - -`ErrorResponse` - -- `detail: str` - -`CommentResponse` - -- `id: str` -- `portfolio_id: str` -- `content: str` -- `created_at: datetime | null` -- `updated_at: datetime | null` - -`BotPrincipalResponse` - -- `id: str` -- `name: str` -- `permission: "read" | "trade-write" | "admin"` -- `description: str | null` -- `is_active: bool` -- `created_at: datetime | null` -- `updated_at: datetime | null` -- `deactivated_at: datetime | null` - -## 9. CLI Specification - -Global option: - -- `--format [table|json]` (default: `table`) - -Commands: - -- `portfoliohub init-db [--maintenance-db postgres]` -- `portfoliohub migrate-db` -- `portfoliohub health-check` - -Portfolio subcommands: - -- `portfoliohub portfolio list [--broker ...] [--status ...]` -- `portfoliohub portfolio status [--recent 5]` -- `portfoliohub portfolio create --symbol ... [--name ...] [options] [--force]` -- `portfoliohub portfolio update [options] [--force]` -- `portfoliohub portfolio buy --quantity ... --price ... [options] [--force]` -- `portfoliohub portfolio sell --quantity ... --price ... [options] [--force]` -- `portfoliohub portfolio invest --type --amount ... [options] [--force]` -- `portfoliohub portfolio transfer --direction --amount ... [options] [--force]` -- `portfoliohub portfolio reset [--infer-symbol ...] [--force]` -- `portfoliohub portfolio delete [--keep-ledger] [--force]` - -Portfolio comment subcommands: - -- `portfoliohub portfolio comment add --content ` -- `portfoliohub portfolio comment list ` -- `portfoliohub portfolio comment update --content [--force]` -- `portfoliohub portfolio comment delete [--force]` - -Strategy subcommands: - -- `portfoliohub strategy list` -- `portfoliohub strategy create --name ... --config ...` -- `portfoliohub strategy update [--name ...] [--config ...]` -- `portfoliohub strategy delete [--force]` - -Confirmation behavior: - -- Mutating portfolio commands request confirmation by default. -- `--force` bypasses confirmation prompts. - -## 10. Persistence and Migration Specification - -Repository selection: - -- If `PORTFOLIOHUB_DB_ENABLED=false`: an unimplemented placeholder repository raises `FeatureNotReadyError` on access. -- If `PORTFOLIOHUB_DB_ENABLED=true`: `PostgresPortfolioRepository` is used. - -Migration commands: - -- `init-db`: create target DB if missing, then apply Alembic `head` -- `migrate-db`: apply Alembic `head` to existing target DB - -Migration source of truth: - -- `migrations/versions` - -DB schema specification: - -- Detailed table/column/index constraints are documented in `docs/db-schema-spec.md`. -- The documentation is generated from and must stay consistent with Alembic revisions under `migrations/versions`. - -## 11. Error Handling - -Application-level errors are translated through registered exception handlers. - -Typical error classes include: - -- authorization failures (`401`, `403`) -- domain/repository-level failures surfaced by service operations -- feature-not-ready errors when DB mode is disabled but persistence is required - -## 12. CORS Policy - -CORS middleware is enabled with: - -- `allow_origins` from configured origins list -- `allow_credentials = true` -- `allow_methods = ["GET", "POST", "PATCH", "DELETE", "OPTIONS"]` -- `allow_headers = ["Authorization", "Content-Type"]` - -## 13. Testing Specification - -Test framework: - -- `pytest` - -Configured behavior: - -- test paths: `tests` -- recursion excluded: `temp`, `.git`, `.venv` - -Current test modules include API, auth, CLI, and domain service coverage. - -## 14. Versioning and Compatibility - -- Current package version: `0.1.0` -- API version exposure follows application version setting (`app_version`) -- Minimum Python version: `3.11` - -## 15. Future Extension Points - -- Redis-backed cache/session integration -- Loki logging pipeline integration -- Additional brokers and order-execution adapters -- Enhanced operational/admin endpoints diff --git a/docs/technical/api-contracts.md b/docs/technical/api-contracts.md new file mode 100644 index 0000000..061173e --- /dev/null +++ b/docs/technical/api-contracts.md @@ -0,0 +1,663 @@ +# API Contracts + +- Related REQ: `REQ-001`, `REQ-002`, `REQ-003` + +## 공통 원칙 +- 응답은 JSON 기반으로 반환한다. +- 인증이 필요한 엔드포인트는 `Authorization: Bearer `을 사용한다. +- 권한 계층은 `read` < `trade-write` < `admin`이다. +- 에러 응답은 HTTP 상태 코드와 함께 원인 메시지를 제공한다. +- Base URL 예시는 `http://localhost:8000`을 기준으로 한다. +- JSON 예시는 대표 형태이며 일부 선택 필드를 생략할 수 있다. + +## Contract Surface + +### 공개 문서 엔드포인트 +- `GET /docs` +- `GET /redoc` +- `GET /openapi.json` + +### 헬스 +- `GET /health` (인증 없음) +- 정상 응답 예시: +```json +{"status": "ok"} +``` + +### 인증 +- `POST /auth/login` +- `GET /auth/tokens` +- `POST /auth/tokens` +- `DELETE /auth/tokens/{token_id}` +- `POST /auth/bots` +- `GET /auth/bots` +- `POST /auth/bots/{bot_id}/tokens` +- `GET /auth/bots/{bot_id}/tokens` +- `DELETE /auth/bots/{bot_id}/tokens/{token_id}` +- `DELETE /auth/bots/{bot_id}` + +### 포트폴리오 +- `GET /portfolios` +- `GET /portfolios/{portfolio_id}` +- `POST /portfolios` +- `PATCH /portfolios/{portfolio_id}` +- `POST /portfolios/{portfolio_id}/buy` +- `POST /portfolios/{portfolio_id}/sell` +- `POST /portfolios/{portfolio_id}/invest` +- `POST /portfolios/{portfolio_id}/transfer` +- `POST /portfolios/{portfolio_id}/reset` +- `DELETE /portfolios/{portfolio_id}` + +### 포트폴리오 댓글 (`REQ-001`) +- `POST /portfolios/{portfolio_id}/comments` +- `GET /portfolios/{portfolio_id}/comments` +- `PATCH /portfolios/{portfolio_id}/comments/{comment_id}` +- `DELETE /portfolios/{portfolio_id}/comments/{comment_id}` + +### 전략셋 +- `GET /strategy-sets` +- `POST /strategy-sets` +- `GET /strategy-sets/{strategy_set_id}` +- `PATCH /strategy-sets/{strategy_set_id}` +- `DELETE /strategy-sets/{strategy_set_id}` + +### 사용자 +- `GET /users` +- `GET /users/{user_id}` +- `POST /users` +- `PATCH /users/{user_id}` +- `DELETE /users/{user_id}` + +## 오류 처리 기준 + +| 시나리오 | HTTP | +| --- | --- | +| 인증 헤더 누락/형식 오류 | `401` | +| 유효하지 않은 토큰 | `401` | +| 권한 부족 | `403` | +| 유효성 검증 실패 | `422` | +| 리소스 없음 | `404` | +| 내부 예외 | `500` | + +## 상세 엔드포인트 명세 + +### 헬스 + +#### `GET /health` +- 인증 없음 +- 응답 `200`: + +```json +{"status": "ok"} +``` + +### 포트폴리오 + +#### `GET /portfolios` +- Auth: `read` +- Query: + - `broker: string | null` + - `status_filter: string | null` +- 응답 `200`: `PortfolioListItem[]` + +예시: + +```bash +curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled" \ + -H "Authorization: Bearer " +``` + +```json +[ + { + "portfolio_id": "krw-btc", + "broker": "upbit", + "status": "enabled", + "name": "Bitcoin", + "symbol": "KRW-BTC", + "cash_balance": 1000000.0, + "asset_quantity": 0.1, + "price": 100000000.0, + "profit_buffer": 0.0, + "active_strategy_id": "stv-001", + "avabile_strategies": ["stv-001"], + "created_at": "2026-04-03T00:00:00Z", + "updated_at": "2026-04-03T00:00:00Z", + "last_trade_at": "2026-04-03T00:00:00Z", + "total_value": 11000000.0 + } +] +``` + +#### `GET /portfolios/{portfolio_id}` +- Auth: `read` +- Query: + - `recent_trades: int = 5` +- 응답 `200`: `PortfolioDetailResponse` + +```bash +curl -X GET "http://localhost:8000/portfolios/krw-btc?recent_trades=5" \ + -H "Authorization: Bearer " +``` + +```json +{ + "portfolio": { + "portfolio_id": "krw-btc", + "broker": "upbit", + "status": "enabled", + "name": "Bitcoin", + "symbol": "KRW-BTC", + "cash_balance": 1000000.0, + "asset_quantity": 0.1, + "price": 100000000.0, + "profit_buffer": 0.0, + "active_strategy_id": "stv-001", + "avabile_strategies": ["stv-001"], + "created_at": "2026-04-03T00:00:00Z", + "updated_at": "2026-04-03T00:00:00Z", + "last_trade_at": "2026-04-03T00:00:00Z" + }, + "recent_entries": [ + { + "portfolio_id": "krw-btc", + "recorded_at": "2026-04-03T00:00:00Z", + "action": "buy", + "price": 100000000.0, + "quantity": 0.1, + "fee_amount": 0.0, + "tax_amount": 0.0, + "cash_balance_after": 1000000.0, + "asset_quantity_after": 0.1, + "total_value_after": 11000000.0, + "profit_buffer_after": 0.0, + "strategy_set_id": "stv-001", + "note": "" + } + ], + "strategy_set": { + "id": "stv-001", + "name": "target_value", + "config": "{}", + "nick": "target_value:99914b932bd3", + "created_at": "2026-04-03T00:00:00Z" + }, + "live_quantity": null +} +``` + +#### `POST /portfolios` +- Auth: `trade-write` +- Body: `CreatePortfolioRequest` +- 응답 `201`: `PortfolioDetailResponse` + +```bash +curl -X POST "http://localhost:8000/portfolios" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "portfolio_id": "krw-btc", + "broker": "upbit", + "status": "enabled", + "name": "Bitcoin", + "symbol": "KRW-BTC", + "strategy_name": "target_value", + "strategy_params": {}, + "active_strategy_id": null, + "avabile_strategies": null, + "cash_balance": 1000000, + "asset_quantity": 0, + "asset_price": 0, + "profit_buffer": 0 + }' +``` + +#### `PATCH /portfolios/{portfolio_id}` +- Auth: `trade-write` +- Body: `UpdatePortfolioRequest` +- 응답 `200`: `PortfolioDetailResponse` + +```bash +curl -X PATCH "http://localhost:8000/portfolios/krw-btc" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "broker": "upbit", + "name": "Bitcoin", + "symbol": "KRW-BTC" + }' +``` + +#### 거래/자금 이동 엔드포인트 +- `POST /portfolios/{portfolio_id}/buy`: `BuyRequest` -> `PortfolioDetailResponse` +- `POST /portfolios/{portfolio_id}/sell`: `SellRequest` -> `PortfolioDetailResponse` +- `POST /portfolios/{portfolio_id}/invest`: `InvestRequest` -> `PortfolioDetailResponse` +- `POST /portfolios/{portfolio_id}/transfer`: `TransferRequest` -> `PortfolioDetailResponse` +- `POST /portfolios/{portfolio_id}/reset`: `ResetPortfolioRequest` -> `PortfolioDetailResponse` +- `DELETE /portfolios/{portfolio_id}`: admin only, query `keep_ledger: bool = false`, 응답 `204` + +대표 요청: + +```bash +curl -X POST "http://localhost:8000/portfolios/krw-btc/buy" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"quantity": 0.01, "price": 100000000, "fee": 0, "tax": 0, "note": ""}' + +curl -X POST "http://localhost:8000/portfolios/krw-btc/sell" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"quantity": 0.01, "price": 100000000, "fee": 0, "tax": 0, "note": ""}' + +curl -X POST "http://localhost:8000/portfolios/krw-btc/invest" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"movement_type": "deposit", "amount": 50000, "note": ""}' + +curl -X POST "http://localhost:8000/portfolios/krw-btc/transfer" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"direction": "to-buffer", "amount": 10000, "note": ""}' + +curl -X POST "http://localhost:8000/portfolios/krw-btc/reset" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"infer_symbol": "KRW-BTC"}' + +curl -X DELETE "http://localhost:8000/portfolios/krw-btc?keep_ledger=false" \ + -H "Authorization: Bearer " +``` + +#### 포트폴리오 댓글 (`REQ-001`) +- `POST /portfolios/{portfolio_id}/comments` + - Auth: `trade-write` + - Body: `CommentCreateRequest` + - Response `201`: `CommentResponse` +- `GET /portfolios/{portfolio_id}/comments` + - Auth: `read` + - Response `200`: `CommentResponse[]` +- `PATCH /portfolios/{portfolio_id}/comments/{comment_id}` + - Auth: `trade-write` + - Body: `CommentUpdateRequest` + - Response `200`: `CommentResponse` +- `DELETE /portfolios/{portfolio_id}/comments/{comment_id}` + - Auth: `trade-write` + - Response `204` + +예시: + +```bash +curl -X POST "http://localhost:8000/portfolios/krw-btc/comments" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"content": "Bought more BTC today.\nLooking good."}' + +curl -X GET "http://localhost:8000/portfolios/krw-btc/comments" \ + -H "Authorization: Bearer " + +curl -X PATCH "http://localhost:8000/portfolios/krw-btc/comments/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"content": "Updated note: BTC rally continues."}' + +curl -X DELETE "http://localhost:8000/portfolios/krw-btc/comments/a1b2c3d4-e5f6-7890-abcd-ef1234567890" \ + -H "Authorization: Bearer " +``` + +```json +{ + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "portfolio_id": "krw-btc", + "content": "Bought more BTC today.\nLooking good.", + "created_at": "2026-04-10T00:00:00Z", + "updated_at": "2026-04-10T00:00:00Z" +} +``` + +### 전략셋 + +#### `GET /strategy-sets` +- Auth: `read` +- Response `200`: `StrategySetResponse[]` + +#### `POST /strategy-sets` +- Auth: `trade-write` +- Body: `CreateStrategySetRequest` +- Response `201`: `StrategySetResponse` +- 기존 `(name, config)` 쌍이 있으면 기존 strategy set 반환 + +#### `PATCH /strategy-sets/{strategy_set_id}` +- Auth: `trade-write` +- Body: `UpdateStrategySetRequest` +- Response `200`: `StrategySetResponse` + +#### `DELETE /strategy-sets/{strategy_set_id}` +- Auth: `trade-write` +- Response `204` +- in use 상태면 `409` + +대표 요청: + +```bash +curl -X POST "http://localhost:8000/strategy-sets" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"name": "target_value", "config": "{}"}' + +curl -X PATCH "http://localhost:8000/strategy-sets/stv-001" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"config": "{\"rebalance\": true}"}' + +curl -X DELETE "http://localhost:8000/strategy-sets/stv-001" \ + -H "Authorization: Bearer " +``` + +### 인증 + +#### `POST /auth/login` +- Auth 없음 +- Body: `LoginRequest` +- Response `200`: `TokenResponse` +- 잘못된 username/password면 `401` + +```bash +curl -X POST "http://localhost:8000/auth/login" \ + -H "Content-Type: application/json" \ + -d '{ + "username": "admin", + "password": "bootstrap123" + }' +``` + +```json +{ + "access_token": "plain-token-value", + "token_type": "bearer", + "expires_at": "2026-04-03T12:00:00Z", + "permission": "admin", + "user": { + "id": "user-1", + "username": "admin", + "email": "admin@example.com", + "permission": "admin", + "is_active": true, + "created_at": "2026-04-03T00:00:00Z", + "updated_at": "2026-04-03T00:00:00Z", + "last_login_at": "2026-04-03T00:00:00Z" + } +} +``` + +#### `GET /auth/tokens` +- Auth: DB-issued `read` 이상 토큰 +- Response `200`: `AuthTokenResponse[]` + +#### `POST /auth/tokens` +- Auth: DB-issued `read` 이상 토큰 +- Body: `CreateClientTokenRequest` +- Response `201`: `IssuedClientTokenResponse` +- 요청 permission이 현재 permission을 넘으면 `403` + +#### `DELETE /auth/tokens/{token_id}` +- Auth: DB-issued `read` 이상 토큰 +- Response `204` +- 미존재 시 `404` + +대표 요청: + +```bash +curl -X POST "http://localhost:8000/auth/tokens" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"name": "cli", "permission": "trade-write", "expires_in_days": 30}' +``` + +#### 봇 주체와 봇 토큰 +- `POST /auth/bots`: admin only, `CreateBotPrincipalRequest` -> `BotPrincipalResponse` +- `GET /auth/bots`: admin only, `is_active` 필터 지원 +- `GET /auth/bots/{bot_id}`: admin only +- `DELETE /auth/bots/{bot_id}`: admin only, bot 비활성화 및 연결 토큰 revoke, 응답 `200` +- `POST /auth/bots/{bot_id}/tokens`: admin only, `CreateBotTokenRequest` -> `IssuedBotTokenResponse` +- `GET /auth/bots/{bot_id}/tokens`: admin only, `AuthTokenResponse[]` +- `DELETE /auth/bots/{bot_id}/tokens/{token_id}`: admin only, `204` + +대표 요청: + +```bash +curl -X POST "http://localhost:8000/auth/bots" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"name": "rebalance-bot", "permission": "trade-write", "description": "nightly jobs"}' + +curl -X POST "http://localhost:8000/auth/bots/bot-1/tokens" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"name": "nightly", "permission": "trade-write", "expires_in_days": 90}' +``` + +### 사용자 + +모든 `/users` 엔드포인트는 admin only이다. + +- `GET /users`: `UserResponse[]` +- `GET /users/{user_id}`: `UserResponse`, 미존재 시 `404` +- `POST /users`: `CreateUserRequest` -> `UserResponse`, duplicate username/email이면 `409` +- `PATCH /users/{user_id}`: `UpdateUserRequest` -> `UserResponse`, duplicate면 `409` +- `DELETE /users/{user_id}`: `204` + +bootstrap 규칙: + +- DB persistence가 활성이고 `PORTFOLIOHUB_BOOTSTRAP_USERNAME`, `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`가 설정되면, `users` 테이블이 비어 있을 때만 startup에서 초기 admin user를 생성한다. + +대표 요청: + +```bash +curl -X GET "http://localhost:8000/users" \ + -H "Authorization: Bearer " + +curl -X POST "http://localhost:8000/users" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "username": "alice", + "email": "alice@example.com", + "password": "password123", + "permission": "read", + "is_active": true + }' +``` + +## 요청/응답 모델 요약 + +### Request Models + +`CreatePortfolioRequest` + +- `portfolio_id: str` +- `broker: str = "kis"` +- `status: str = "enabled"` +- `name: str | null` +- `symbol: str` +- `strategy_name: str = "target_value"` +- `strategy_params: object = {}` +- `active_strategy_id: str | null` +- `avabile_strategies: string[] | null` +- `cash_balance: float >= 0` +- `asset_quantity: float >= 0` +- `asset_price: float >= 0` +- `profit_buffer: float >= 0` + +`UpdatePortfolioRequest` + +- `broker: str | null` +- `status: str | null` +- `name: str | null` +- `symbol: str | null` +- `strategy_name: str | null` +- `strategy_params: object | null` +- `active_strategy_id: str | null` +- `avabile_strategies: string[] | null` +- `asset_price: float >= 0 | null` + +`BuyRequest` / `SellRequest` + +- `quantity: float > 0` + +`InvestRequest` + +- `movement_type: "deposit" | "profit" | "withdraw"` +- `amount: float > 0` +- `note: str = ""` + +`TransferRequest` + +- `direction: "to-buffer" | "from-buffer"` +- `amount: float > 0` +- `note: str = ""` + +`ResetPortfolioRequest` + +- `infer_symbol: str | null` + +`CommentCreateRequest` / `CommentUpdateRequest` + +- `content: str` + +`LoginRequest` + +- `username: str` +- `password: str` + +`CreateClientTokenRequest` + +- `name: str` +- `permission: "read" | "trade-write" | "admin" | null` +- `expires_in_days: int | null` + +`CreateBotPrincipalRequest` + +- `name: str` +- `permission: "read" | "trade-write" | "admin"` +- `description: str | null` + +`CreateBotTokenRequest` + +- `name: str` +- `permission: "read" | "trade-write" | "admin" | null` +- `expires_in_days: int | null` + +`CreateUserRequest` + +- `username: str` +- `email: str | null` +- `password: str` +- `permission: "read" | "trade-write" | "admin"` +- `is_active: bool` + +`UpdateUserRequest` + +- `username: str | null` +- `email: str | null` +- `password: str | null` +- `permission: "read" | "trade-write" | "admin" | null` +- `is_active: bool | null` + +`CreateStrategySetRequest` + +- `name: str` +- `config: str` + +`UpdateStrategySetRequest` + +- `name: str | null` +- `config: str | null` + +### Response Models + +`PortfolioListItem` + +- portfolio summary fields + computed `total_value` + +`PortfolioSummary` + +- `portfolio_id: str` +- `broker: str` +- `status: str` +- `name: str` +- `symbol: str` +- `cash_balance: float` +- `asset_quantity: float` +- `price: float` +- `profit_buffer: float` +- `active_strategy_id: str | null` +- `avabile_strategies: string[] | null` +- `created_at: datetime | null` +- `updated_at: datetime | null` +- `last_trade_at: datetime | null` + +`PortfolioDetailResponse` + +- `portfolio: PortfolioSummary` +- `recent_entries: LedgerEntryResponse[]` +- `strategy_set: StrategySetResponse | null` +- `live_quantity: float | null` + +`StrategySetResponse` + +- `id: str` +- `name: str` +- `config: str` +- `nick: str` +- `created_at: datetime | null` + +`CommentResponse` + +- `id: str` +- `portfolio_id: str` +- `content: str` +- `created_at: datetime | null` +- `updated_at: datetime | null` + +`BotPrincipalResponse` + +- `id: str` +- `name: str` +- `permission: "read" | "trade-write" | "admin"` +- `description: str | null` +- `is_active: bool` +- `created_at: datetime | null` +- `updated_at: datetime | null` +- `deactivated_at: datetime | null` + +`ErrorResponse` + +- `detail: str` + +## 공통 오류 예시 + +### Missing token + +```json +{"detail": "Not authenticated"} +``` + +### Invalid token + +```json +{"detail": "Invalid token"} +``` + +### Insufficient permission + +```json +{"detail": "Insufficient permission"} +``` + +## 문서 동기화 규칙 +- `app/api/` 또는 `app/schemas/` 변경 시 본 문서를 같은 변경 단위에서 갱신한다. +- `REQ-002` 태그 검색 API가 구현되면 요청/응답 파라미터와 필터 의미를 본 문서에 추가한다. + +## Open Implementation Points +- 에러 응답 body를 공통 포맷(`error_code`, `message`, `details`)으로 강제할지 결정한다. +- idempotency가 필요한 쓰기 API(예: 재시도 가능 매매 요청)의 키 정책을 확정한다. diff --git a/docs/technical/architecture-overview.md b/docs/technical/architecture-overview.md new file mode 100644 index 0000000..699c83a --- /dev/null +++ b/docs/technical/architecture-overview.md @@ -0,0 +1,113 @@ +# Architecture Overview + +- Related REQ: `REQ-003` + +## 목표 +`portfolio-hub`는 포트폴리오 상태 관리 기능을 API와 CLI에 동일하게 제공하고, 서비스/도메인/리포지토리 계층 분리를 통해 변경 비용을 낮춘다. + +## 개요와 범위 + +런타임/구성요소 기준: + +- Runtime: Python 3.11+ +- API framework: FastAPI +- CLI framework: Typer +- DB driver: Psycopg v3 +- Migration: Alembic + +주요 목표: + +- 포트폴리오 lifecycle 관리(create/update/delete/reset) +- 거래 및 잔고 영향 액션 기록(buy/sell/invest/transfer) +- API와 CLI에서 동일한 상태/ledger 조회 제공 +- 공유 service/repository 계층을 통한 동작 일관성 유지 + +현재 범위: + +- 인증된 포트폴리오 조회/변경 작업 +- 관리자 기반 사용자 CRUD +- 권한 계층(read, trade-write, admin) +- feature flag 기반 PostgreSQL/Timescale 저장소 +- DB bootstrap 및 migration 운영 명령 + +현재 비목표 / 제한: + +- production 용도 메모리 fallback repository 미구현 +- 외부 broker 주문 실행 미포함 +- Redis/Loki 관측성 경로는 완전한 runtime wiring 미완료 + +## 상위 구성 요소 + +| 컴포넌트 | 책임 | +| --- | --- | +| `app/main.py` | FastAPI 앱 생성, 라우터 등록, 설정 초기화 | +| `app/api/` | 인증/헬스/포트폴리오/전략셋/사용자 라우트 | +| `app/cli/` | 운영용 명령 인터페이스와 확인 프롬프트 | +| `app/services/` | API/CLI 공통 유스케이스 수행 | +| `app/domain/` | 포트폴리오 거래 규칙, 밸런스 계산 | +| `app/repositories/` | 저장소 추상화 및 Postgres 구현 | +| `app/db/`, `migrations/` | DB 초기화 및 스키마 버전 관리 | + +## 엔트리포인트 + +HTTP 서비스: + +- App factory: `app.main:create_app` +- ASGI app: `app.main:app` +- OpenAPI docs: `/docs` +- ReDoc: `/redoc` + +CLI: + +- Console script: `portfoliohub` +- Entry function: `app.cli.main:main` + +## 기본 런타임 흐름 + +### 1. API 기반 거래 처리 +1. 라우트에서 요청 바디와 권한을 검증한다. +2. 서비스 계층에서 도메인 규칙(수량/금액/상태)을 평가한다. +3. 리포지토리에서 포트폴리오/원장 데이터를 갱신한다. +4. 응답 스키마로 직렬화해 반환한다. + +### 2. CLI 기반 동일 작업 처리 +1. CLI 명령이 인자를 파싱하고 확인 프롬프트를 처리한다. +2. API와 동일한 서비스 메서드를 호출한다. +3. 결과를 터미널 친화 포맷으로 출력한다. + +### 3. DB 활성화 경로 +1. `PORTFOLIOHUB_DB_ENABLED=true` 확인 +2. DB 연결 정보(`TIMESCALEDB_*`) 로딩 +3. Postgres 리포지토리 선택 +4. 필요 시 Alembic 마이그레이션 후 서비스 기동 + +## 설계 원칙 +- **Service Reuse First**: API/CLI는 동일 service 계층을 재사용한다. +- **Contract Stability**: 외부 계약은 `app/schemas/`와 기술 문서에서 일관성을 유지한다. +- **Persistence Isolation**: 영속성 구현 세부사항은 repository 계층에 격리한다. +- **Fail Fast Config**: 필수 설정 누락은 조기 실패로 드러낸다. + +## 테스트와 호환성 기준 + +테스트 기준: + +- 테스트 프레임워크는 `pytest`를 사용한다. +- 주요 테스트 범위는 API, auth, CLI, domain service를 포함한다. +- 재귀 제외 경로는 `temp`, `.git`, `.venv`를 사용한다. + +버전/호환성 기준: + +- 현재 패키지 버전: `0.1.0` +- API 버전 노출은 앱 버전 설정(`app_version`)을 따른다. +- 최소 Python 버전: `3.11` + +## 향후 확장 포인트 + +- Redis 기반 cache/session 통합 +- Loki logging pipeline 통합 +- 추가 broker 및 order-execution adapter +- 운영/admin 확장 엔드포인트 + +## Open Implementation Points +- `REQ-002` 태그 검색 구현 시 조회 성능 요구를 만족하는 인덱스/쿼리 전략을 확정한다. +- 관측성(메트릭/로그)의 표준 이벤트 스키마를 별도 문서로 확장할지 결정한다. diff --git a/docs/technical/auth-and-token-lifecycle.md b/docs/technical/auth-and-token-lifecycle.md new file mode 100644 index 0000000..7893a28 --- /dev/null +++ b/docs/technical/auth-and-token-lifecycle.md @@ -0,0 +1,54 @@ +# Auth and Token Lifecycle + +- Related REQ: N/A + +## 목표 +정적 토큰과 DB 발급 토큰을 병행하는 현재 인증 모델의 동작 기준을 명확히 한다. + +## 권한 모델 + +| Permission | 허용 범위 | +| --- | --- | +| `read` | 조회 계열 API | +| `trade-write` | 조회 + 포트폴리오 변경 API | +| `admin` | 전체 API + 사용자/봇 관리 | + +## 인증 방식 + +- 보호된 API는 `Authorization: Bearer ` 헤더를 사용한다. +- 인증 소스는 정적 토큰과 DB 저장 토큰을 병행한다. + +권한 계층: + +- `admin`은 전체 엔드포인트 접근 가능 +- `trade-write`는 `trade-write`와 `read` 범위 접근 가능 +- `read`는 조회 엔드포인트만 접근 가능 + +## 인증 소스 +- 환경변수 기반 정적 토큰: `PORTFOLIOHUB_*_TOKENS` +- DB 저장 토큰: 로그인 세션/클라이언트 토큰/봇 토큰 + +## 토큰 라이프사이클 +1. 사용자 로그인(`POST /auth/login`) 또는 토큰 발급 API 호출 +2. 토큰 해시 저장 및 prefix 관리 +3. 요청 처리 시 해시 비교와 만료/폐기 상태 검증 +4. `DELETE /auth/tokens/{token_id}` 또는 봇 비활성화로 토큰 폐기 + +## 실패 응답 기준 + +- bearer 헤더 누락 또는 형식 오류: `401` +- 알 수 없는 토큰: `401` +- 권한 부족: `403` + +## 부트스트랩 사용자 규칙 +- `PORTFOLIOHUB_BOOTSTRAP_USERNAME`과 `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`가 설정되고 users 테이블이 비어 있으면 초기 admin을 1회 생성한다. +- 이미 사용자가 존재하면 추가 생성하지 않는다. + +## Validation Notes +- 만료된 토큰은 항상 인증 실패로 처리해야 한다. +- 비활성 봇(`is_active=false`)의 토큰은 거부되어야 한다. +- 권한 계층 승격 우회가 없는지 API/CLI 양쪽에서 검증한다. + +## Open Implementation Points +- 정적 토큰과 DB 토큰의 우선순위/병합 정책을 명문화할지 결정한다. +- 감사 로그(audit trail)에 토큰 발급/폐기 이벤트를 표준 포맷으로 남길지 확정한다. diff --git a/docs/technical/deployment-and-operations.md b/docs/technical/deployment-and-operations.md new file mode 100644 index 0000000..e7b9a55 --- /dev/null +++ b/docs/technical/deployment-and-operations.md @@ -0,0 +1,193 @@ +# Deployment and Operations + +- Related REQ: N/A + +## 목표 +PortfolioHub를 Linux 환경에서 안정적으로 배포하고 HTTPS 종단, 프로세스 복구, 기본 점검 경로를 표준화한다. + +## 사전 조건 + +- public IP를 가진 Linux/Ubuntu 서버 +- 해당 IP를 가리키는 도메인 DNS `A` record +- 방화벽에서 `80`, `443` 포트 개방 + +## 권장 배포 토폴로지 +- App: `uvicorn app.main:app` +- Reverse proxy/TLS: Caddy +- Process manager: systemd +- 선택: Docker Compose + Caddy sidecar + +## Linux Direct Run 기준 +1. Python 가상환경 생성 후 패키지를 설치한다. +2. 환경 변수 또는 `.env`를 구성한다. +3. `uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2`를 실행한다. +4. `GET /health`를 확인한다. + +## Caddy 설치와 구성 + +Ubuntu/Debian 설치 예시: + +```bash +sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \ + | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg +curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \ + | sudo tee /etc/apt/sources.list.d/caddy-stable.list +sudo apt update +sudo apt install caddy +``` + +설치 검증: + +```bash +caddy version +``` + +기본 Caddyfile: + +```text +your-domain.com { + reverse_proxy localhost:8000 +} +``` + +동작 기준: + +- TLS 인증서 자동 발급/갱신 +- `80`에서 `443`으로 HTTP redirect +- 외부 요청을 내부 `uvicorn` 프로세스(`127.0.0.1:8000`)로 전달 + +적용 후 reload: + +```bash +sudo systemctl reload caddy +``` + +## systemd 운영 기준 +- 서비스 계정 권한 최소화 +- `Restart=on-failure` +- 로그 수집 경로(journalctl 또는 외부 수집기) 확보 + +예시 unit 파일: + +```ini +[Unit] +Description=PortfolioHub API +After=network.target + +[Service] +User=www-data +WorkingDirectory=/opt/portfoliohub +EnvironmentFile=/opt/portfoliohub/.env +ExecStart=/opt/portfoliohub/.venv/bin/uvicorn app.main:app \ + --host 127.0.0.1 --port 8000 --workers 2 +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` + +등록/기동: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable portfoliohub +sudo systemctl start portfoliohub +``` + +## HTTPS/Caddy 운영 기준 +- 도메인 DNS가 서버 IP를 가리켜야 한다. +- 포트 `80/443`을 개방한다. +- Caddyfile에서 앱을 `reverse_proxy`로 연결한다. +- 브라우저 기반 frontend가 있으면 `PORTFOLIOHUB_ALLOWED_ORIGINS`에 정확한 HTTPS origin을 지정한다. + +예시: + +```bash +PORTFOLIOHUB_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com +``` + +## Docker Compose 운영 기준 +- 앱 컨테이너는 내부 포트만 expose하고 외부 종단은 Caddy가 담당한다. +- `caddy_data`, `caddy_config` 볼륨을 유지해 인증서 상태를 보존한다. +- 배포 후 `docker compose ps`, `curl /health`로 기본 상태를 확인한다. + +예시 compose: + +```yaml +services: + app: + build: . + expose: + - "8000" + environment: + - PORTFOLIOHUB_ALLOWED_ORIGINS=https://your-domain.com + restart: unless-stopped + + caddy: + image: caddy:2-alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + depends_on: + - app + restart: unless-stopped + +volumes: + caddy_data: + caddy_config: +``` + +compose 네트워크 안에서는 `reverse_proxy app:8000`을 사용한다. + +기동: + +```bash +docker compose up -d +``` + +## 방화벽 설정 + +UFW: + +```bash +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw reload +``` + +firewalld: + +```bash +sudo firewall-cmd --permanent --add-service=http +sudo firewall-cmd --permanent --add-service=https +sudo firewall-cmd --reload +``` + +## 배포 검증 + +HTTPS health check: + +```bash +curl https://your-domain.com/health +``` + +운영 상태 확인: + +```bash +sudo systemctl status caddy +sudo journalctl -u caddy -f +``` + +## Validation Notes +- HTTPS 엔드포인트에서 `/health`, `/docs` 접근이 가능해야 한다. +- DB 사용 배포에서는 마이그레이션 적용 후 앱을 기동한다. +- CORS 오리진 설정과 실제 프론트엔드 도메인이 일치해야 한다. + +## Open Implementation Points +- blue/green 또는 rolling update 같은 무중단 배포 전략 도입 여부를 확정한다. +- Loki/metrics 통합 운영 절차를 별도 런북으로 분리할지 결정한다. diff --git a/docs/technical/index.md b/docs/technical/index.md new file mode 100644 index 0000000..8a36f82 --- /dev/null +++ b/docs/technical/index.md @@ -0,0 +1,49 @@ +# portfolio-hub Technical Specification Index + +## 목적 +- 이 디렉토리는 `portfolio-hub`의 **구현 상세 설계(How)** 를 관리한다. +- 승인 기준과 외부 요구사항의 source of truth는 `docs/SRS/`이며, 기술 문서는 해당 요구사항을 구현하기 위한 내부 설계를 정리한다. + +## 문서 사용 원칙 +1. 외부 계약, acceptance criteria, 상태 전이는 `docs/SRS/`에서 관리한다. +2. 내부 모듈 책임, 시퀀스, 데이터 흐름, 예시 payload는 `docs/technical/`에서 관리한다. +3. 기술 문서가 SRS와 충돌하면, 별도 owner 결정 전까지 **SRS를 우선**한다. +4. 기술 문서를 수정할 때는 관련 REQ ID를 함께 표시해 traceability를 유지한다. + +## 문서 목록 + +| 문서 | 목적 | 연관 REQ | +| --- | --- | --- | +| `project-structure-baseline.md` | 저장소 baseline 디렉토리 구조와 코드/문서 배치 기준 | N/A | +| `python-runtime-and-packaging.md` | Python 런타임과 `pyproject.toml` 기반 패키징 기준 | N/A | +| `settings-and-env-loading.md` | `.env`/환경변수 로딩, 필수 키, CORS/운영 설정 규칙 | N/A | +| `architecture-overview.md` | 시스템 개요, 모듈 책임, 런타임 흐름, 테스트/호환성 기준 | `REQ-003` | +| `api-contracts.md` | HTTP API 상세 계약, request/response 모델, 대표 payload 예시 | `REQ-001`, `REQ-002`, `REQ-003` | +| `persistence-and-migrations.md` | PostgreSQL/Timescale 연동, 리포지토리 스위치, Alembic 운영, 스키마 baseline | `REQ-001`, `REQ-003` | +| `auth-and-token-lifecycle.md` | 로그인/토큰/봇 주체 라이프사이클 및 권한 모델 | N/A | +| `management-cli-operations.md` | `portfoliohub` CLI 명령 책임, 종료코드, 운영 플로우 | N/A | +| `deployment-and-operations.md` | Linux systemd, Caddy reverse proxy, Docker Compose 운영 절차 | N/A | + +## 연관 REQ 매핑 표 + +| REQ ID | Technical Docs | +| --- | --- | +| `REQ-001` | `api-contracts.md`, `persistence-and-migrations.md` | +| `REQ-002` | `api-contracts.md` | +| `REQ-003` | `architecture-overview.md`, `api-contracts.md`, `persistence-and-migrations.md` | + +## 권장 작성 순서 +1. `project-structure-baseline.md` +2. `python-runtime-and-packaging.md` +3. `settings-and-env-loading.md` +4. `architecture-overview.md` +5. `api-contracts.md` +6. `persistence-and-migrations.md` +7. `auth-and-token-lifecycle.md` +8. `management-cli-operations.md` +9. `deployment-and-operations.md` + +## 추적 규칙 +- 각 기술 문서는 상단에 관련 REQ를 명시한다. +- 구현 착수 전에는 해당 문서의 open issue가 남아 있는지 확인한다. +- 운영 중 발견된 차이는 technical 문서에 먼저 반영하고, 외부 계약 변화가 있으면 SRS 업데이트 여부를 별도로 판정한다. diff --git a/docs/technical/management-cli-operations.md b/docs/technical/management-cli-operations.md new file mode 100644 index 0000000..d30e425 --- /dev/null +++ b/docs/technical/management-cli-operations.md @@ -0,0 +1,84 @@ +# Management CLI Operations + +- Related REQ: N/A + +## 목표 +`portfoliohub` CLI를 운영 자동화와 수동 점검 모두에 안전하게 사용할 수 있도록 명령 책임과 사용 규칙을 정의한다. + +## 명령 그룹 + +| 그룹 | 주요 명령 | 목적 | +| --- | --- | --- | +| DB 운영 | `init-db`, `migrate-db`, `health-check` | 스키마 적용 및 연결 상태 점검 | +| 사용자 관리 | `user list/get/create/update/delete` | 운영 계정 라이프사이클 | +| 포트폴리오 조회 | `portfolio list`, `portfolio status` | 상태 확인 | +| 포트폴리오 변경 | `portfolio create/update/buy/sell/invest/transfer/reset/delete` | 거래/자금/상태 변경 | + +## 인터페이스 명세 + +전역 옵션: + +- `--format [table|json]` (기본값: `table`) + +공통 명령: + +- `portfoliohub init-db [--maintenance-db postgres]` +- `portfoliohub migrate-db` +- `portfoliohub health-check` + +포트폴리오 하위 명령: + +- `portfoliohub portfolio list [--broker ...] [--status ...]` +- `portfoliohub portfolio status [--recent 5]` +- `portfoliohub portfolio create --symbol ... [--name ...] [options] [--force]` +- `portfoliohub portfolio update [options] [--force]` +- `portfoliohub portfolio buy --quantity ... --price ... [options] [--force]` +- `portfoliohub portfolio sell --quantity ... --price ... [options] [--force]` +- `portfoliohub portfolio invest --type --amount ... [options] [--force]` +- `portfoliohub portfolio transfer --direction --amount ... [options] [--force]` +- `portfoliohub portfolio reset [--infer-symbol ...] [--force]` +- `portfoliohub portfolio delete [--keep-ledger] [--force]` + +포트폴리오 댓글 하위 명령: + +- `portfoliohub portfolio comment add --content ` +- `portfoliohub portfolio comment list ` +- `portfoliohub portfolio comment update --content [--force]` +- `portfoliohub portfolio comment delete [--force]` + +전략셋 하위 명령: + +- `portfoliohub strategy list` +- `portfoliohub strategy create --name ... --config ...` +- `portfoliohub strategy update [--name ...] [--config ...]` +- `portfoliohub strategy delete [--force]` + +## 실행 원칙 +- 쓰기 명령은 기본적으로 확인 프롬프트를 띄운다. +- 자동화 스크립트에서는 `--force`를 사용해 비대화식 실행을 보장한다. +- DB 타깃 실행 시 `PORTFOLIOHUB_DB_ENABLED=true`와 `TIMESCALEDB_*`를 명시한다. + +## 예시 시나리오 +1. 신규 환경 준비 + - `portfoliohub init-db` + - `portfoliohub health-check` +2. 일상 운영 + - `portfoliohub portfolio list` + - `portfoliohub portfolio status ` +3. 배포 후 검증 + - `portfoliohub user list` + - 주요 API smoke test와 결과 비교 + +## 오류/종료코드 가이드 +- `0`: 정상 종료 +- `1`: 입력/검증/런타임 오류 +- 재시도 가능 오류(예: 일시적 DB 연결 실패)는 메시지에 원인을 명확히 표기한다. + +확인 동작: + +- 변경 명령은 기본적으로 확인 프롬프트를 요청한다. +- `--force`를 지정하면 확인 프롬프트를 생략한다. + +## Open Implementation Points +- JSON 출력 모드(`--json`)를 도입해 자동화 친화성을 높일지 결정한다. +- 대량 작업(batch) 명령 추가 시 실패 항목 보고 포맷을 표준화한다. diff --git a/docs/technical/persistence-and-migrations.md b/docs/technical/persistence-and-migrations.md new file mode 100644 index 0000000..9eec26a --- /dev/null +++ b/docs/technical/persistence-and-migrations.md @@ -0,0 +1,279 @@ +# Persistence and Migrations + +- Related REQ: `REQ-001`, `REQ-003` + +## 목표 +DB 기반 영속성 경로를 안정적으로 운영하고, 스키마 변경 이력과 문서 동기화를 일관되게 유지한다. + +## 저장소 선택 규칙 +- `PORTFOLIOHUB_DB_ENABLED=true`이면 Postgres 리포지토리를 사용한다. +- DB 비활성 모드는 개발/부트스트랩 목적의 제한적 동작으로 간주한다. + +비활성 동작 기준: + +- `PORTFOLIOHUB_DB_ENABLED=false`이면 미구현 placeholder repository가 `FeatureNotReadyError`를 발생시킨다. + +## 데이터 모델 핵심 영역 + +| 테이블 | 역할 | +| --- | --- | +| `portfolios` | 현재 포트폴리오 상태 | +| `portfolio_ledger` | 거래/자금이동 이력 스냅샷 | +| `portfolio_comments` | 포트폴리오 코멘트 이력 (`REQ-001`) | +| `strategy_sets` | 전략 설정 스냅샷 | +| `users` | 운영 사용자 | +| `auth_tokens` | 세션/클라이언트/봇 토큰 | +| `bot_principals` | 외부 봇 주체 | + +## 스키마 문서 기준 + +- Source of truth: Alembic revisions in `migrations/versions` +- Current baseline revision: `20260410_0009` +- 상세 테이블/컬럼/인덱스 제약은 이 문서가 Alembic revision과 동기화된 기준 문서다. + +## 마이그레이션 운영 흐름 +1. 신규 스키마 변경을 Alembic revision으로 추가한다. +2. `migrations/versions/`에 revision 파일을 저장한다. +3. 본 문서에 revision ID와 스키마 변경 내역을 반영한다. +4. `portfoliohub migrate-db` 또는 Alembic `upgrade head`로 적용한다. + +## 명령 기준 +- 초기 DB 준비: `portfoliohub init-db` +- 버전 업그레이드: `portfoliohub migrate-db` +- 상태 점검: `portfoliohub health-check` + +명령 동작: + +- `init-db`: target DB가 없으면 생성 후 Alembic `head` 적용 +- `migrate-db`: 기존 target DB에 Alembic `head` 적용 + +## 스키마 기준 + +### Conventions + +- 데이터 타입 표기는 SQLAlchemy/Alembic 선언명을 따른다. +- `NULL`은 nullable을 의미한다. +- Default는 DB-level `server_default`를 의미한다. +- FK 제약이 없어도 logical relationship column은 문서화한다. + +### Tables + +#### users + +Purpose: + +- 운영 사용자 레코드와 향후 identity mapping 기준 저장 + +| Column | Type | Null | Default | Constraints / Notes | +| --- | --- | --- | --- | --- | +| id | Text | NO | - | Primary Key | +| username | Text | NO | - | Unique username | +| email | Text | YES | - | Unique when present | +| permission | Text | NO | `read` | User permission level | +| is_active | Boolean | NO | TRUE | Whether login is allowed | +| password_hash | Text | YES | - | Scrypt password hash | +| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | +| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp | +| last_login_at | DateTime(timezone=True) | YES | - | Last successful login timestamp | + +Constraints: + +- Primary key: `id` +- Unique: `uq_users_username` on (`username`) +- Unique: `uq_users_email` on (`email`) + +#### auth_tokens + +Purpose: + +- DB 발급 session/client/bot token 저장 + +| Column | Type | Null | Default | Constraints / Notes | +| --- | --- | --- | --- | --- | +| id | Text | NO | - | Primary Key | +| user_id | Text | NO | - | Logical user reference | +| name | Text | NO | - | Human-readable token name | +| token_prefix | Text | NO | - | Display prefix | +| token_hash | Text | NO | - | Unique SHA-256 token hash | +| permission | Text | NO | - | Effective token permission | +| token_type | Text | NO | - | `session`, `client`, or `bot` | +| bot_principal_id | Text | YES | - | Logical reference to `bot_principals.id` | +| expires_at | DateTime(timezone=True) | YES | - | Expiration time | +| last_used_at | DateTime(timezone=True) | YES | - | Last successful use | +| revoked_at | DateTime(timezone=True) | YES | - | Revocation time | +| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | + +Constraints: + +- Primary key: `id` +- Unique: `uq_auth_tokens_token_hash` on (`token_hash`) + +#### bot_principals + +Purpose: + +- 외부 bot identity와 권한 ceiling 관리 + +| Column | Type | Null | Default | Constraints / Notes | +| --- | --- | --- | --- | --- | +| id | Text | NO | - | Primary Key | +| name | Text | NO | - | Unique bot name | +| permission | Text | NO | - | Bot permission ceiling | +| description | Text | YES | - | Optional description | +| is_active | Boolean | NO | TRUE | False면 bot token 거부 | +| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | +| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp | +| deactivated_at | DateTime(timezone=True) | YES | - | Deactivation time | + +Constraints: + +- Primary key: `id` +- Unique: `uq_bot_principals_name` on (`name`) + +#### strategy_sets + +Purpose: + +- 정규화된 전략 설정 snapshot 저장 + +| Column | Type | Null | Default | Constraints / Notes | +| --- | --- | --- | --- | --- | +| id | Text | NO | - | Primary Key | +| name | Text | NO | - | Strategy name, unique pair 일부 | +| config | Text | NO | `""` | Strategy config payload | +| nick | Text | NO | - | Unique nickname | +| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | + +Constraints: + +- Primary key: `id` +- Unique: `uq_strategy_sets_name_config` on (`name`, `config`) +- Unique: `uq_strategy_sets_nick` on (`nick`) + +#### portfolios + +Purpose: + +- 현재 포트폴리오 상태와 strategy linkage 저장 + +Current baseline note: + +- 아래 컬럼은 revision `20260403_0008`까지의 마이그레이션 기준이다. + +| Column | Type | Null | Default | Constraints / Notes | +| --- | --- | --- | --- | --- | +| portfolio_id | Text | NO | - | Primary Key | +| broker | Text | NO | `kis` | Broker code | +| status | Text | NO | `enabled` | Portfolio status | +| name | Text | NO | - | Asset name | +| symbol | Text | NO | - | Trading symbol | +| cash_balance | Float | NO | - | Current cash balance | +| asset_quantity | Float | NO | - | Current asset quantity | +| asset_price | Float | NO | 0 | Current price snapshot | +| profit_buffer | Float | NO | 0 | Profit buffer amount | +| active_strategy_id | Text | YES | - | Logical reference to `strategy_sets.id` | +| avabile_strategies | Text[] | YES | - | Nullable array of `strategy_sets.id` | +| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | +| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp | +| last_trade_at | DateTime(timezone=True) | YES | - | Last trade time | + +Constraints: + +- Primary key: `portfolio_id` +- Foreign key constraints: none in baseline + +#### portfolio_ledger + +Purpose: + +- 포트폴리오 액션별 append-like ledger snapshot 저장 + +| Column | Type | Null | Default | Constraints / Notes | +| --- | --- | --- | --- | --- | +| portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` | +| recorded_at | DateTime(timezone=True) | NO | - | Event timestamp | +| action | Text | NO | - | Action type | +| price | Float | NO | - | Trade/valuation unit price | +| quantity | Float | NO | - | Quantity delta | +| fee_amount | Float | NO | 0 | Fees | +| tax_amount | Float | NO | 0 | Taxes | +| cash_balance_after | Float | NO | - | Post-action cash | +| asset_quantity_after | Float | NO | - | Post-action asset quantity | +| total_value_after | Float | NO | - | Post-action total value | +| profit_buffer_after | Float | NO | 0 | Post-action profit buffer | +| strategy_set_id | Text | NO | `legacy` | Logical strategy set reference | +| note | Text | NO | `""` | Operator/system note | + +Constraints: + +- Primary key: none in baseline +- Foreign key constraints: none in baseline + +#### portfolio_comments + +Purpose: + +- 포트폴리오별 multiline comment history 저장 (`REQ-001`) + +| Column | Type | Null | Default | Constraints / Notes | +| --- | --- | --- | --- | --- | +| id | Text | NO | - | Primary Key (UUID) | +| portfolio_id | Text | NO | - | Logical reference to `portfolios.portfolio_id` | +| content | Text | NO | - | Multiline comment text | +| created_at | DateTime(timezone=True) | NO | NOW() | Creation timestamp | +| updated_at | DateTime(timezone=True) | NO | NOW() | Last update timestamp | + +Constraints: + +- Primary key: `id` +- Foreign key constraints: none + +### Indexes + +| Index | Table | Columns/Expressions | Unique | Intent | +| --- | --- | --- | --- | --- | +| `ix_users_created_at` | `users` | `created_at` | no | recency listing | +| `idx_auth_tokens_user_created` | `auth_tokens` | `user_id`, `created_at` | no | current user token listing | +| `idx_auth_tokens_hash_active` | `auth_tokens` | `token_hash` | no | bearer token lookup | +| `idx_auth_tokens_bot_created` | `auth_tokens` | `bot_principal_id`, `created_at` | no | bot token listing | +| `idx_bot_principals_active_created` | `bot_principals` | `is_active`, `created_at` | no | active/inactive bot filtering | +| `idx_portfolio_ledger_portfolio_time` | `portfolio_ledger` | `portfolio_id`, `recorded_at DESC` | no | recent ledger query | +| `idx_portfolios_broker_status_updated` | `portfolios` | `broker`, `status`, `updated_at DESC` | no | broker/status recency filtering | +| `idx_portfolio_comments_portfolio_created` | `portfolio_comments` | `portfolio_id`, `created_at DESC` | no | reverse chronological comments | + +Conditional migration behavior: + +- `timescaledb` extension 존재 시 `create_hypertable('portfolio_ledger', 'recorded_at', if_not_exists => TRUE)` 실행 +- 이 경우 `portfolio_ledger`는 hypertable로 동작한다. + +## Integrity Notes + +현재 baseline은 DB-level FK constraint를 의도적으로 두지 않는다. + +- logical relation은 application/repository logic이 관리한다. +- 향후 FK constraint를 추가하면 migration ID와 함께 본 문서를 갱신한다. +- application layer cascade: + - 포트폴리오 삭제 시 연관 `portfolio_comments`를 hard delete 한다. + +## Validation Notes +- 마이그레이션은 빈 DB와 기존 DB 모두에서 연속 적용 가능해야 한다. +- 파괴적 변경(drop/rename)은 백필 또는 호환 단계 계획 없이 직접 반영하지 않는다. +- 문서 동기화 게이트(`scripts/check_docs_sync.py`)를 통과해야 한다. + +## Revision Mapping + +| Revision ID | File | Summary | +| --- | --- | --- | +| 20260403_0001 | migrations/versions/20260403_0001_initial_portfolio_schema.py | Initial schema for strategy sets, portfolios, ledger, indexes, optional hypertable | +| 20260403_0002 | migrations/versions/20260403_0002_strategy_set_columns.py | Rename `strategy_name` to `name`, `strategy_config` to `config`, add unique `nick` | +| 20260403_0003 | migrations/versions/20260403_0003_portfolio_strategy_refs.py | Remove embedded strategy fields, add `active_strategy_id` and `avabile_strategies` | +| 20260403_0004 | migrations/versions/20260403_0004_users_table.py | Add `users` table with unique username/email and created_at index | +| 20260403_0005 | migrations/versions/20260403_0005_auth_user_fields_and_tokens.py | Add user auth fields and `auth_tokens` table | +| 20260403_0006 | migrations/versions/20260403_0006_bot_principals_and_bot_tokens.py | Add `bot_principals` and bot token linkage | +| 20260403_0007 | migrations/versions/20260403_0007_bot_principal_permission_only.py | Remove `owner_user_id` and enforce bot-local permission model | +| 20260403_0008 | migrations/versions/20260403_0008_portfolio_name_column.py | Add non-null `portfolios.name` and backfill from `symbol` | +| 20260410_0009 | migrations/versions/20260410_0009_portfolio_comments_table.py | Add `portfolio_comments` table for per-portfolio memo history | + +## Open Implementation Points +- `portfolio_ledger`의 Timescale hypertable 강제 여부와 보존 정책(retention)을 확정한다. +- `REQ-002` 태그 검색 구현 시 인덱스 전략(B-tree/GiN 등)과 마이그레이션 단계를 추가 정의한다. diff --git a/docs/technical/project-structure-baseline.md b/docs/technical/project-structure-baseline.md new file mode 100644 index 0000000..8b4ce6d --- /dev/null +++ b/docs/technical/project-structure-baseline.md @@ -0,0 +1,39 @@ +# Project Structure Baseline + +- Related REQ: N/A + +## 목표 +`portfolio-hub` 구현 및 유지보수 시 디렉토리 책임을 명확히 하여 API/CLI/도메인/저장소 변경의 영향 범위를 예측 가능하게 유지한다. + +## Target Baseline Structure + +| 경로 | 책임 | 구현 메모 | +| --- | --- | --- | +| `docs/SRS/` | 외부 계약, 승인 기준, REQ 상태 관리 | requirement source of truth | +| `docs/technical/` | 구현 상세 설계와 운영 메모 | SRS를 재정의하지 않음 | +| `app/api/` | HTTP 라우트, 요청/응답 경계 | 서비스 계층 호출 전담 | +| `app/cli/` | Typer CLI 명령과 입출력 | 서비스 계층 재사용 | +| `app/services/` | 유스케이스 조합, 권한/검증 흐름 | API/CLI 공통 오케스트레이션 | +| `app/domain/` | 포트폴리오 비즈니스 규칙 | 기술 의존성 최소화 | +| `app/repositories/` | 영속성 인터페이스와 구현체 | DB feature flag 지원 | +| `app/schemas/` | API 계약 스키마(Pydantic) | 요청/응답 안정성 | +| `migrations/` | Alembic 마이그레이션 | 스키마 변경 이력 | +| `tests/` | API/도메인/CLI/계약 테스트 | 회귀 방지 게이트 | + +## Root-Level Exception Rules +- 루트에는 `README.md`, `pyproject.toml`, `alembic.ini`, `.github/` 등 저장소 메타파일만 둔다. +- 실행 코드와 테스트 코드는 목적 디렉토리로 수렴시킨다. + +## Documentation Responsibilities +- `README.md`: 빠른 시작, 실행 방법, 운영 필수 변수 요약 +- `docs/SRS/`: 왜(Why), 무엇(What), 수락 기준 +- `docs/technical/`: 어떻게(How), 모듈 경계, 예시 흐름 + +## Validation Notes +- 신규 모듈 추가 시 책임이 상기 경로 정의와 충돌하지 않는지 확인한다. +- API 계약 변경 시 `app/schemas/`와 `docs/technical/api-contracts.md`를 함께 동기화한다. +- 스키마 변경 시 `migrations/versions/`와 문서 업데이트를 같은 변경 단위에 포함한다. + +## Open Implementation Points +- Redis/Loki 실제 운영 경로가 코드에 전면 반영되면 `app/core/` 또는 별도 infra 패키지 분리 여부를 확정한다. +- 태그 검색 기능(`REQ-002`) 구현 시 서비스/리포지토리 경계의 책임 분리를 추가 명세한다. diff --git a/docs/technical/python-runtime-and-packaging.md b/docs/technical/python-runtime-and-packaging.md new file mode 100644 index 0000000..5c04da1 --- /dev/null +++ b/docs/technical/python-runtime-and-packaging.md @@ -0,0 +1,33 @@ +# Python Runtime and Packaging + +- Related REQ: N/A + +## 목표 +Python `3.11+` 및 `pyproject.toml` 중심 패키징 기준을 유지하고, 로컬/CI/운영의 실행 환경 편차를 줄인다. + +## Runtime Baseline +- 공식 지원 런타임은 Python `3.11` 이상이다. +- 로컬 개발은 `venv` 기반 실행을 기본으로 한다. +- 개발/테스트 의존성은 editable install(`pip install -e .[dev]`)로 일관화한다. + +## Required Files + +| 파일 | 책임 | 필수 내용 | +| --- | --- | --- | +| `pyproject.toml` | 프로젝트 메타데이터와 의존성 기준 | `requires-python >=3.11` | +| `README.md` | 설치/실행/테스트 진입점 | venv 생성 및 실행 절차 | +| `alembic.ini` | DB migration 런타임 설정 | Alembic 동작 기준 | + +## Packaging Rules +- 패키징 source of truth는 `pyproject.toml`로 유지한다. +- CLI 엔트리포인트 `portfoliohub`는 패키지 설치 후 동일하게 동작해야 한다. +- 운영 이미지/환경에서도 개발과 동일한 Python major/minor를 사용한다. + +## Validation Notes +- `pip install -e .[dev]` 후 `uvicorn app.main:app --reload`와 `pytest`가 실행 가능해야 한다. +- `portfoliohub --help`가 정상 출력되어야 한다. +- Alembic 명령(`upgrade head`)이 현재 마이그레이션 체인에서 실패하지 않아야 한다. + +## Open Implementation Points +- 락파일 기반 재현성(`uv lock`, `poetry.lock` 등) 도입 여부는 팀 운영 정책에 따라 확정한다. +- 배포 환경 컨테이너 표준화 시 base image pinning 정책을 문서화한다. diff --git a/docs/technical/settings-and-env-loading.md b/docs/technical/settings-and-env-loading.md new file mode 100644 index 0000000..6485800 --- /dev/null +++ b/docs/technical/settings-and-env-loading.md @@ -0,0 +1,84 @@ +# Settings and Env Loading + +- Related REQ: N/A + +## 목표 +설정값 로딩 경로를 단일화하여 로컬/운영 간 동작 차이를 최소화하고, 보안 민감 값을 안전하게 주입한다. + +## 설정 로딩 우선순위 +1. 환경 변수 +2. `.env` 파일 +3. 코드 기본값 + +## 핵심 설정 그룹 + +| 그룹 | 예시 키 | 목적 | +| --- | --- | --- | +| API/CORS | `PORTFOLIOHUB_ALLOWED_ORIGINS` | 브라우저 호출 허용 오리진 제어 | +| 인증/권한 | `PORTFOLIOHUB_READ_TOKENS`, `PORTFOLIOHUB_TRADE_WRITE_TOKENS`, `PORTFOLIOHUB_ADMIN_TOKENS` | 정적 토큰 기반 접근 제어 | +| 세션/토큰 TTL | `PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS` 등 | DB 발급 토큰 만료 정책 | +| Bootstrap User | `PORTFOLIOHUB_BOOTSTRAP_USERNAME`, `PORTFOLIOHUB_BOOTSTRAP_PASSWORD` | 초기 admin 사용자 생성 | +| DB 연결 | `TIMESCALEDB_*` | Postgres/Timescale 접속 정보 | +| Observability | `LOKI_*`, `REDIS_*` | 중앙 로그/캐시 운영 확장 포인트 | + +## 상세 설정 명세 + +핵심 설정: + +- `PORTFOLIOHUB_ALLOWED_ORIGINS`: comma-separated CORS origins +- `PORTFOLIOHUB_BOOTSTRAP_USERNAME`: users 테이블이 비어 있을 때 생성할 초기 username +- `PORTFOLIOHUB_BOOTSTRAP_EMAIL`: bootstrap user의 선택적 email +- `PORTFOLIOHUB_BOOTSTRAP_PASSWORD`: 초기 admin password +- `PORTFOLIOHUB_READ_TOKENS`: comma-separated read tokens +- `PORTFOLIOHUB_TRADE_WRITE_TOKENS`: comma-separated trade-write tokens +- `PORTFOLIOHUB_ADMIN_TOKENS`: comma-separated admin tokens +- `PORTFOLIOHUB_DB_ENABLED`: `true`일 때 DB repository 활성화 +- `PORTFOLIOHUB_SESSION_TOKEN_TTL_HOURS`: login session token TTL(hours) +- `PORTFOLIOHUB_CLIENT_TOKEN_DEFAULT_TTL_DAYS`: client token 기본 TTL(days) +- `PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS`: bot token 기본 TTL(days) + +DB 활성 설정: + +- `TIMESCALEDB_HOST` +- `TIMESCALEDB_PORT` +- `TIMESCALEDB_NAME` +- `TIMESCALEDB_USER` +- `TIMESCALEDB_PASSWORD` +- `TIMESCALEDB_SSLMODE` +- `TIMESCALEDB_CONNECT_TIMEOUT_SEC` +- `TIMESCALEDB_APPLICATION_NAME` + +파생 설정: + +- `active_database_url`은 psycopg PostgreSQL URL 포맷으로 credentials와 query options를 인코딩해 구성한다. + +## CORS 정책 + +CORS middleware는 다음 기준으로 동작한다. + +- `allow_origins`: 구성된 origins 목록 사용 +- `allow_credentials = true` +- `allow_methods = ["GET", "POST", "PATCH", "DELETE", "OPTIONS"]` +- `allow_headers = ["Authorization", "Content-Type"]` + +브라우저 기반 frontend가 HTTPS로 접근하면 `PORTFOLIOHUB_ALLOWED_ORIGINS`에는 정확한 HTTPS origin만 명시한다. + +예시: + +```bash +PORTFOLIOHUB_ALLOWED_ORIGINS=https://app.example.com,https://www.example.com +``` + +## 운영 규칙 +- 운영 환경에서는 `.env` 대신 배포 플랫폼의 환경 변수 주입을 우선한다. +- `PORTFOLIOHUB_DB_ENABLED=true`일 때 `TIMESCALEDB_*` 필수 키 누락 여부를 시작 시 검증한다. +- 민감 값은 문서/샘플에 실제 비밀 값을 기록하지 않는다. + +## Validation Notes +- 필수 키 누락 시 명확한 예외 메시지로 기동 실패해야 한다. +- CORS 오리진 파싱은 공백/쉼표 변형 입력에도 안정적으로 동작해야 한다. +- 부트스트랩 계정 자동 생성은 `users` 테이블이 비어 있을 때만 수행되어야 한다. + +## Open Implementation Points +- 설정 스키마를 strict mode로 고정할지(미정의 키 거부) 여부를 확정한다. +- Redis/Loki 활성화 플래그와 실패 시 동작(무시/실패)을 명시적으로 분리할지 결정한다. diff --git a/scripts/check_docs_sync.py b/scripts/check_docs_sync.py index 7e315db..959f2a2 100644 --- a/scripts/check_docs_sync.py +++ b/scripts/check_docs_sync.py @@ -2,11 +2,11 @@ """Guardrail check for implementation-to-spec synchronization. Rules: -- If files under app/ change, docs/portfoliohub-spec.md must be part of the same change set. -- If files under app/api/ or app/schemas/ change, docs/api-examples.md must be part of the same change set. -- If files under migrations/versions/ change, docs/db-schema-spec.md must be part of the same change set. +- If files under app/ change, at least one docs/technical/ file must be part of the same change set. +- If files under app/api/ or app/schemas/ change, docs/technical/api-contracts.md must be part of the same change set. +- If files under migrations/versions/ change, docs/technical/persistence-and-migrations.md must be part of the same change set. - If migrations/versions/ files change, revision IDs found in changed migration files - must appear in docs/db-schema-spec.md. + must appear in docs/technical/persistence-and-migrations.md. """ from __future__ import annotations @@ -18,9 +18,9 @@ import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] -GENERAL_SPEC = "docs/portfoliohub-spec.md" -DB_SPEC = "docs/db-schema-spec.md" -API_EXAMPLES_SPEC = "docs/api-examples.md" +TECHNICAL_DOC_PREFIX = "docs/technical/" +DB_SPEC = "docs/technical/persistence-and-migrations.md" +API_EXAMPLES_SPEC = "docs/technical/api-contracts.md" APP_PREFIX = "app/" API_PREFIXES = ("app/api/", "app/schemas/") MIGRATION_PREFIX = "migrations/versions/" @@ -110,32 +110,32 @@ def run_check(*, staged: bool, against: str | None, head: str) -> int: errors: list[str] = [] - if needs_general_spec and GENERAL_SPEC not in changed_files: + if needs_general_spec and not any(path.startswith(TECHNICAL_DOC_PREFIX) for path in changed_files): errors.append( - "Application code changed but docs/portfoliohub-spec.md was not updated in the same commit/PR." + "Application code changed but no docs/technical/ document was updated in the same commit/PR." ) if needs_api_examples and API_EXAMPLES_SPEC not in changed_files: errors.append( - "API/schema code changed but docs/api-examples.md was not updated in the same commit/PR." + "API/schema code changed but docs/technical/api-contracts.md was not updated in the same commit/PR." ) if needs_db_spec and DB_SPEC not in changed_files: errors.append( - "Migration changed but docs/db-schema-spec.md was not updated in the same commit/PR." + "Migration changed but docs/technical/persistence-and-migrations.md was not updated in the same commit/PR." ) if needs_db_spec: db_spec_path = ROOT / DB_SPEC if not db_spec_path.exists(): - errors.append("docs/db-schema-spec.md is missing.") + errors.append("docs/technical/persistence-and-migrations.md is missing.") else: db_spec_text = db_spec_path.read_text(encoding="utf-8") changed_revisions = _collect_changed_revision_ids(changed_files) missing_revisions = [rev for rev in changed_revisions if rev not in db_spec_text] if missing_revisions: errors.append( - "Migration revision IDs missing in docs/db-schema-spec.md: " + "Migration revision IDs missing in docs/technical/persistence-and-migrations.md: " + ", ".join(sorted(set(missing_revisions))) ) From acd1bab8f2737640f3d6209d44f0aa790c267c04 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Mon, 13 Apr 2026 23:25:23 +0900 Subject: [PATCH 03/21] Squashed '.vendor/myagent-kit/' content from commit 96defc9 git-subtree-dir: .vendor/myagent-kit git-subtree-split: 96defc92f92d617a80f7b0f54e998ce4c25f631b --- README.md | 152 ++++++++++ .../implementation-from-approved-req.md | 72 +++++ base/.github/copilot-instructions.md | 16 ++ ...open-requirement-discovery.instructions.md | 50 ++++ ...requirement-scope-decision.instructions.md | 52 ++++ base/.github/instructions/srs.instructions.md | 53 ++++ .../prompts/approve-requirement.prompt.md | 26 ++ .../prompts/complete-requirement.prompt.md | 32 +++ .../create-implementation-issue.prompt.md | 42 +++ .../decide-requirement-scope.prompt.md | 38 +++ .../mark-requirement-implemented.prompt.md | 25 ++ .../reopen-requirement-status.prompt.md | 44 +++ base/.github/prompts/sync-srs-index.prompt.md | 28 ++ .../prompts/sync-technical-specs.prompt.md | 34 +++ .../prompts/validate-requirement.prompt.md | 24 ++ base/.github/pull_request_template.md | 24 ++ bootstrap/install.sh | 263 ++++++++++++++++++ templates/docs/SRS/REQ-template.md | 55 ++++ templates/docs/SRS/TRACEABILITY-template.md | 28 ++ 19 files changed, 1058 insertions(+) create mode 100644 README.md create mode 100644 base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md create mode 100644 base/.github/copilot-instructions.md create mode 100644 base/.github/instructions/reopen-requirement-discovery.instructions.md create mode 100644 base/.github/instructions/requirement-scope-decision.instructions.md create mode 100644 base/.github/instructions/srs.instructions.md create mode 100644 base/.github/prompts/approve-requirement.prompt.md create mode 100644 base/.github/prompts/complete-requirement.prompt.md create mode 100644 base/.github/prompts/create-implementation-issue.prompt.md create mode 100644 base/.github/prompts/decide-requirement-scope.prompt.md create mode 100644 base/.github/prompts/mark-requirement-implemented.prompt.md create mode 100644 base/.github/prompts/reopen-requirement-status.prompt.md create mode 100644 base/.github/prompts/sync-srs-index.prompt.md create mode 100644 base/.github/prompts/sync-technical-specs.prompt.md create mode 100644 base/.github/prompts/validate-requirement.prompt.md create mode 100644 base/.github/pull_request_template.md create mode 100755 bootstrap/install.sh create mode 100644 templates/docs/SRS/REQ-template.md create mode 100644 templates/docs/SRS/TRACEABILITY-template.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..394215f --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# myagent-kit + +Reusable Copilot Development workflow assets. + +## Contents +- `base/.github/prompts`: reusable prompts +- `base/.github/instructions`: reusable file instructions +- `base/.github/ISSUE_TEMPLATE`: reusable issue templates +- `base/.github/copilot-instructions.md`: managed Copilot baseline gate template (opt-in merge) +- `base/.github/pull_request_template.md`: PR traceability gate template +- `templates/docs/SRS/REQ-template.md`: REQ starter template +- `templates/docs/SRS/TRACEABILITY-template.md`: traceability matrix starter template +- `bootstrap/install.sh`: copy base assets into a target project + +### Pre-flight Scope Decision (Step 0) +새 기능이나 변경 요청이 들어오면, requirement drafting을 시작하기 **전에** 먼저 아래 명령으로 SRS 대상 여부를 판정한다. + +| Asset | 용도 | +|---|---| +| `base/.github/prompts/decide-requirement-scope.prompt.md` | 변경 요청을 입력받아 4종 판정 결과 반환 | +| `base/.github/instructions/requirement-scope-decision.instructions.md` | 판정 기준 및 출력 형식 가이드 | +| `base/.github/prompts/reopen-requirement-status.prompt.md` | `Approved/Implemented/Deprecated` requirement를 `Proposed`(또는 `Re-review`)로 되돌리기 위한 결정/충돌 검토 | +| `base/.github/instructions/reopen-requirement-discovery.instructions.md` | REQ 미지정 입력 시 후보 탐색(키워드/점수/동점 처리) 커스터마이징 가이드 | + +**판정 결과 4종** +1. `New SRS requirement needed` — 새 REQ 문서 작성 +2. `Update existing SRS requirement` — 기존 REQ 문서 수정 +3. `Technical spec only` — SRS 변경 없이 technical spec만 업데이트 +4. `Needs owner decision` — 운영 리스크/책임이 불명확하여 소유자 결정 필요 + +## Install into a project + +```bash +bash .vendor/myagent-kit/bootstrap/install.sh /path/to/project +``` + +변경 적용 없이 계획만 확인하려면 dry-run을 사용한다. + +```bash +bash .vendor/myagent-kit/bootstrap/install.sh /path/to/project --dry-run +``` + +옵션: 기존 `.github/copilot-instructions.md`를 덮어쓰지 않고 병합하려면 opt-in 플래그를 사용한다. + +```bash +bash .vendor/myagent-kit/bootstrap/install.sh /path/to/project --merge-copilot-instructions +``` + +병합 정책 요약: +- marker 이름은 `myagent-kit`로 고정된다. +- 기존 파일이 있으면 `.github/copilot-instructions.md.bak`으로 최근 1개 백업 후 병합한다. +- 병합은 `myagent-kit` 관리 섹션만 갱신하고 `project-specific` 섹션은 유지한다. +- marker 쌍이 손상된 경우 병합을 중단하고 백업본으로 롤백한다. + +In subtree-based usage: + +```bash +git subtree add --prefix=.vendor/myagent-kit https://github.com//myagent-kit.git main --squash +bash .vendor/myagent-kit/bootstrap/install.sh . +bash .vendor/myagent-kit/bootstrap/install.sh . --dry-run +bash .vendor/myagent-kit/bootstrap/install.sh . --merge-copilot-instructions +bash .vendor/myagent-kit/bootstrap/install.sh . --merge-copilot-instructions --dry-run +``` + +## 운영 규칙 + +- SRS는 계약 문서, 구현 문서는 실행 기록으로 분리합니다. +- 모든 요구사항에 고유 ID를 부여합니다. 예: REQ-001 +- 구현/테스트/릴리즈까지 같은 ID를 계속 사용합니다. +- 상태 기반으로 관리합니다: Draft → Approved → Implemented → Verified → Released + +## 추적/운영 표준 + +### 1) 식별자 규칙 +- Requirement ID는 `REQ-XXX` 형식을 사용합니다. 예: `REQ-001` +- Acceptance Criteria는 requirement 내부에서 `AC1`, `AC2`처럼 번호를 고정합니다. +- 이슈/PR 제목에는 requirement ID를 포함합니다. 예: `[REQ-001] ...` + +### 2) 증거(Evidence) 연결 규칙 +- 구현 이슈에는 `Acceptance Criteria Mapping`을 작성합니다. (`ACx -> Task`) +- PR에는 어떤 AC가 어떤 변경으로 충족되었는지 명시합니다. +- 테스트 이름 또는 테스트 설명에도 requirement ID를 포함합니다. +- 릴리즈 노트에는 포함된 requirement ID 목록을 남깁니다. + +### 3) 추적 문서 운영 +- 추적 매트릭스는 `docs/SRS/traceability-matrix.md`로 운영합니다. +- 초기 작성은 `templates/docs/SRS/TRACEABILITY-template.md`를 복사해 사용합니다. +- 매트릭스의 각 행은 최소한 `REQ ID`, `AC`, `Issue/PR`, `Test`, `Status`를 채웁니다. + +### 4) 최소 운영 절차 +1. Requirement 작성 및 ID 확정 +2. Requirement를 `Approved`로 전환 +3. Implementation Issue 생성 (AC 매핑 포함) +4. PR 생성 (REQ/AC 충족 근거 기입) +5. 테스트 증거 업데이트 +6. Requirement 상태를 `Implemented`/`Verified`로 전환 +7. 릴리즈 시 매트릭스와 릴리즈 노트에 REQ ID 반영 + +## myagent-kit Development Life Cycle + +### Phase 0. Workspace Bootstrap +- 프로젝트에 myagent-kit 자산을 설치한다. +- `bootstrap/install.sh`를 실행해 `.github` 워크플로 자산을 동기화한다. + +### Phase 0.5. Pre-flight Scope Decision *(새 기능/변경 요청 진입 시 필수)* +- `decide-requirement-scope.prompt.md`로 변경 요청이 SRS 대상인지 먼저 판정한다. +- 판정 결과에 따라 다음 단계를 결정한다. + - `New SRS requirement needed` → Phase 1로 진행 + - `Update existing SRS requirement` → 해당 REQ 파일을 수정하고 Phase 2로 진행 + - `Technical spec only` → SRS 변경 없이 technical spec / implementation note만 업데이트 + - `Needs owner decision` → 소유자와 협의 후 위 3가지 중 하나로 재판정 + +### Phase 1. Requirement Drafting +- `templates/docs/SRS/REQ-template.md`로 REQ 문서를 작성한다. +- REQ ID(`REQ-XXX`)를 확정하고, `Acceptance Criteria`를 test 가능한 문장으로 작성한다. + +### Phase 2. Requirement Quality Gate +- `validate-requirement.prompt.md`로 requirement 품질 검토를 수행한다. +- 모호성, 누락 필드, impacted area, change log 누락을 수정한다. + +### Phase 3. Requirement Approval Gate +- `approve-requirement.prompt.md`로 `Proposed -> Approved` 전환 가능 여부를 확인한다. +- 승인 이력(Change Log)을 남기고 상태를 `Approved`로 변경한다. + +### Phase 3.5. Requirement Reopen / Re-review +- 구현 전/후에 계약 변경, acceptance criteria 재정의, 리스크 재평가가 필요하면 `reopen-requirement-status.prompt.md`를 사용한다. +- 입력 예시: `REQ-003, REQ-011; API 응답 필드 계약을 다시 정리해야 해서 재검토 필요`. +- 결과에서 사용자 결정 항목(목표 상태: `Proposed` vs `Re-review`)과 충돌 후보 REQ를 먼저 확인한 뒤 REQ 문서/인덱스를 수정한다. + +### Phase 4. Pre-Implementation Design Review +- Implementation Issue를 생성하고 설계검토 체크리스트를 작성한다. +- 아키텍처, 인터페이스, 데이터, 오류 처리, 보안/성능, 테스트 전략을 합의한다. +- 미해결 항목은 `Open Items / Assumptions`에 기록한다. + +### Phase 5. Implementation Planning +- `create-implementation-issue.prompt.md`로 구현 이슈를 생성한다. +- `Acceptance Criteria Mapping`으로 AC와 작업 항목을 연결한다. +- AI Agent 작업과 Human 작업을 분리한다. + +### Phase 6. Implementation & PR +- 구현을 진행하고 PR을 생성한다. +- `.github/pull_request_template.md`로 REQ ID, AC 커버리지, 테스트 증거를 기록한다. + +### Phase 7. Verification & Status Transition +- 자동/수동 검증 결과를 반영한다. +- `mark-requirement-implemented.prompt.md`로 `Approved -> Implemented` 전환 가능 여부를 점검한다. +- 충족 여부가 확인되면 requirement 상태를 전환한다. + +### Phase 8. Traceability & Release +- `docs/SRS/traceability-matrix.md`를 갱신한다. +- 릴리즈 노트에 반영된 REQ ID를 기록한다. +- 필요 시 운영 문서/회고 문서에 residual risk를 남긴다. diff --git a/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md b/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md new file mode 100644 index 0000000..42e8832 --- /dev/null +++ b/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md @@ -0,0 +1,72 @@ +--- +name: "Implementation From Approved Requirement" +about: "Create an implementation issue from an Approved SRS requirement" +title: "[REQ-XXX] Implement " +labels: ["implementation", "requirement"] +assignees: [] +--- + + + +## 배경 (Context) +- Requirement ID: +- Requirement Link: +- Status Check: Approved + +## 범위 (Scope) +- In Scope: +- Out of Scope: + +## 구현 전 설계검토 체크리스트 (Pre-Implementation Design Review) +- [ ] Architecture approach is documented and agreed +- [ ] Public interface/API contract changes are identified +- [ ] Data model/schema impact is reviewed +- [ ] Error handling and fallback strategy is defined +- [ ] Security/privacy impact is reviewed +- [ ] Performance/scalability impact is reviewed +- [ ] Test strategy (unit/integration/e2e/manual) is defined +- [ ] Rollback or feature-flag strategy is defined when needed +- [ ] Unknowns, assumptions, and decision owners are recorded +- [ ] Human review sign-off completed (or `No additional design review required.`) + +### 설계 결정 요약 (Design Decisions) +- Decision 1: +- Decision 2: + +### 미해결 항목 / 가정 (Open Items / Assumptions) +- + +## AI Agent 작업 (AI Agent Work) +- [ ] Code change task 1 +- [ ] Code change task 2 +- [ ] Automated validation task + +## 사람 작업 (Human Work) +- [ ] Approve scope or open questions +- [ ] Perform required manual verification + +## Acceptance Criteria Mapping +- REQ AC1 -> Task(s): +- REQ AC2 -> Task(s): + +## AI 검증 요약 (AI Validation Summary) +- Automated tests/checks to run: +- Expected evidence: + +## 사람 확인 체크리스트 (Human Verification Checklist) +- [ ] Verify: +- [ ] Confirm: +- If no human verification is required, write: `No manual verification required.` + +## 열린 질문 (Open Questions) +- + +## 리스크 (Risks) +- + +## 완료 조건 (Definition of Done) +- [ ] All mapped acceptance criteria are satisfied +- [ ] Tests are added or updated +- [ ] AI-completable work is finished and evidence is attached +- [ ] Human-only work is completed or explicitly marked not required +- [ ] Docs or change notes are updated if needed diff --git a/base/.github/copilot-instructions.md b/base/.github/copilot-instructions.md new file mode 100644 index 0000000..6ecd55c --- /dev/null +++ b/base/.github/copilot-instructions.md @@ -0,0 +1,16 @@ +# myagent-kit Managed Copilot Instructions + +아래 규칙은 myagent-kit가 관리하는 기본 게이트다. + +## SRS-first Code Change Gate +- 소스 코드 변경이 필요한 요청이면 먼저 `docs/SRS/index.md`와 관련 REQ/SRS 문서를 확인한다. +- SRS와 기술 문서(`docs/technical/`) 기준과 코드 변경 방향이 충돌하면 즉시 사용자에게 선택을 요청한다. +- 사용자가 SRS 변경을 선택하면 `/reopen-requirement-status` 절차를 먼저 수행한다. +- 사용자가 코드 변경 지속을 선택하면 해당 결정을 명시적으로 기록한 뒤 진행한다. + +## No-Conflict Path +- 충돌이 없으면 기술 문서 업데이트가 필요한지 먼저 확인하고, 필요 시 기술 문서를 선반영한 뒤 소스 수정을 진행한다. + +## Traceability +- 코드 변경 시 관련 REQ ID를 확인하고, 가능하면 PR/이슈/테스트 증거와 연결한다. +- 판단이 불충분하면 가정하지 말고 사용자에게 필요한 결정을 질문한다. diff --git a/base/.github/instructions/reopen-requirement-discovery.instructions.md b/base/.github/instructions/reopen-requirement-discovery.instructions.md new file mode 100644 index 0000000..bb579df --- /dev/null +++ b/base/.github/instructions/reopen-requirement-discovery.instructions.md @@ -0,0 +1,50 @@ +--- +description: "Use when customizing REQ candidate discovery rules for /reopen-requirement-status command, including keyword dictionary, scoring, tie-breaks, and normalization policy." +applyTo: ".github/prompts/reopen-requirement-status.prompt.md" +--- +# Reopen Requirement Discovery Guidelines + +## 목적 +`/reopen-requirement-status` 입력에 REQ ID가 없을 때, 관련 REQ 후보를 일관된 기준으로 탐색하고 우선순위를 정한다. + +## 후보 탐색 우선순위 +1. 계약 키워드 일치 +- `API`, `endpoint`, `request`, `response`, `schema`, `field`, `CLI`, `command`, `option`, `UI`, `event`, `config`, `env`, `deploy`, `integration` +2. 영향 영역 일치 +- 입력의 변경 대상 모듈/문서/테스트와 REQ의 `Impacted Area` 교집합 +3. 목적/행동 일치 +- 입력의 동사/의도(추가, 제거, 변경, 완화, 강화, 비활성화 등)와 REQ `Intent`/`Requirement`의 의미 유사도 +4. 상태 가중치 +- 현재 상태가 `Approved`, `Implemented`, `Deprecated`인 REQ 우선 + +## 점수 및 신뢰도 +- 점수식: `키워드(0-5) + 영향영역(0-3) + 의미유사(0-2) + 상태가중치(0-1)` +- 정렬: 총점 내림차순 +- 신뢰도: + - `High(7-11)` + - `Medium(4-6)` + - `Low(0-3)` + +## 도메인 키워드 사전 (커스터마이즈 가능) +- Auth & Access: `auth`, `login`, `oauth`, `token`, `permission`, `rbac`, `role`, `scope` +- Billing & Subscription: `billing`, `invoice`, `payment`, `subscription`, `plan`, `quota`, `credit` +- Data & Migration: `schema`, `migration`, `backfill`, `etl`, `import`, `export`, `retention` +- API & Contract: `api`, `endpoint`, `request`, `response`, `versioning`, `webhook`, `payload` +- Runtime & Operations: `deploy`, `rollback`, `release`, `monitoring`, `alert`, `slo`, `incident` +- Configuration: `config`, `env`, `feature flag`, `toggle`, `secret`, `parameter` +- UX & Interface: `ui`, `ux`, `screen`, `form`, `flow`, `command`, `cli`, `option` +- Testing & Quality: `test`, `e2e`, `regression`, `coverage`, `validation`, `acceptance` + +## 정규화 규칙 +- 사전에 없는 표현이 입력에 등장하면 의미가 유사한 사전 키워드로 1회 정규화해 근거에 표기한다. +- 예: `권한` -> `permission`, `과금` -> `billing` + +## 동점 처리 +- 동일 점수 후보가 여러 개면 최신 Change Log 항목이 있는 REQ를 우선한다. +- 여전히 동점이면 현재 active 상태(`Approved`/`Implemented`) REQ를 우선한다. +- 추가 동점이면 REQ ID가 작은 항목을 우선한다. + +## 출력 권장 +- 후보는 최대 5개 제시 +- 각 후보에 `REQ ID`, `신뢰도`, `점수`, `근거 키워드`, `영향 영역`을 포함 +- 확정 전에는 `Immediate Questions`로 최종 대상 REQ 선택을 먼저 요청 diff --git a/base/.github/instructions/requirement-scope-decision.instructions.md b/base/.github/instructions/requirement-scope-decision.instructions.md new file mode 100644 index 0000000..b3df18a --- /dev/null +++ b/base/.github/instructions/requirement-scope-decision.instructions.md @@ -0,0 +1,52 @@ +--- +description: "Use when evaluating whether a new feature or change request requires a new or updated SRS requirement, a technical spec update only, or needs owner decision. Apply before starting requirement drafting." +applyTo: "docs/SRS/**/*.md" +--- +# Requirement Scope Decision Guidelines + +## 목적 +새 기능 요청이나 변경 사항이 들어왔을 때, SRS에 등록해야 하는지(신규 또는 기존 REQ 수정) 아니면 technical spec / implementation note만 업데이트하면 되는지를 먼저 판정한다. +이 판정을 거침으로써 불필요한 REQ 생성을 방지하고, SRS에 등록돼야 할 변경이 누락되는 것을 예방한다. + +## 판정 결과 분류 (4종) +1. **New SRS requirement needed** — 새 REQ 문서를 작성해야 한다. +2. **Update existing SRS requirement** — 기존 REQ 문서를 수정해야 한다. +3. **Technical spec only** — SRS 변경 없이 technical spec / implementation note만 업데이트한다. +4. **Needs owner decision** — 운영 리스크나 승인 기준이 불명확하여 소유자(Owner) 결정이 필요하다. + +## SRS 대상 판정 기준 (다음 중 하나라도 해당하면 SRS) + +### 외부 계약 변화 +- 사용자 또는 외부 시스템이 관찰하는 계약이 바뀌는 경우 +- API / CLI / UI / Event / Data schema / Config / Deployment / Integration 요구사항이 변경되는 경우 + +### 측정 가능한 Non-Functional 변경 +- 성능, 보안, 가용성, 확장성 등 측정 기준이 추가되거나 변경되는 경우 + +### Acceptance Criteria 및 승인 게이트 필요 +- 변경 사항에 대해 명시적 수락 기준과 승인 이력이 필요한 경우 + +## Technical Spec Only 판정 기준 (다음에 해당하면 SRS 불필요) +- 내부 리팩터링 (외부 계약 변화 없음) +- 라이브러리 교체 (동일한 외부 인터페이스 유지) +- 코드 구조 / 패턴 변경 (기능 동작 동일) +- 외부 계약을 바꾸지 않는 구현 최적화 + +## Borderline / Needs Owner Decision 판정 기준 +- 운영 리스크나 승인 책임이 불명확한 경우 +- 제품 의도가 불분명하거나 범위가 애매한 경우 +- 위 기준만으로 분류가 확정되지 않는 경우 + +## New vs. Update 구분 기준 +- 동일 기능/계약 범위의 변경 → `Update existing SRS requirement` +- 새 기능/계약/인터페이스의 추가 → `New SRS requirement needed` +- 기존 REQ 검색 시 `docs/SRS/index.md`의 Requirement Register를 먼저 확인한다. + +## 판정 출력 형식 +판정 결과는 항상 다음 항목을 포함해야 한다. +1. **Decision** — 위 4종 중 하나 +2. **Why** — 판정 근거 (구체적으로 어떤 기준에 해당하는지) +3. **Recommended Type** — SRS 대상인 경우, 해당 requirement type (Interface / Data / Deployment 등) +4. **Existing REQ candidates** — Update 가능성이 있는 기존 REQ ID 목록 (해당 시) +5. **Next Action** — 판정 결과에 따른 다음 단계 안내 +6. **Open Questions** — 추가 확인이 필요한 사항 (없으면 `없음`) diff --git a/base/.github/instructions/srs.instructions.md b/base/.github/instructions/srs.instructions.md new file mode 100644 index 0000000..d2e3533 --- /dev/null +++ b/base/.github/instructions/srs.instructions.md @@ -0,0 +1,53 @@ +--- +description: "Use when writing or editing SRS documents, requirement files, acceptance criteria, change logs, or impacted area entries under docs/SRS. Ensures requirement quality, traceability, and testable wording." +applyTo: "docs/SRS/**/*.md" +--- +# SRS Document Guidelines + +## Requirement 타입 분류 + +### 분류 의사결정 규칙 (우선순위 순) +1. **Installation**: "시스템 설치, 환경 구성, 초기화 절차를 정의하는가?" → YES면 Installation + - 예: Docker 이미지 빌드, 데이터베이스 마이그레이션, 사전 조건 설정 +2. **Interface**: "사용자가 직접 조작하거나 실행하는 방식을 정의하는가?" → YES면 Interface + - 예: CLI 명령어, API 엔드포인트, 웹 UI 기능, 제어 방식 +3. **Configuration**: "시스템 또는 기능의 설정 옵션, 커스터마이징을 정의하는가?" → YES면 Configuration + - 예: 설정 파라미터, 옵션 항목, 환경 변수, 커스터마이징 포인트 +4. **Data**: "데이터 저장, 로드, 구조, 파일 형식, 스키마를 정의하는가?" → YES면 Data + - 예: 설정 파일 형식, 데이터베이스 스키마, API 요청/응답 구조 +5. **Exception & Error Handling**: "오류 처리, 예외 복구, 재시도 전략을 중심으로 하는가?" → YES면 Exception & Error Handling + - 예: 네트워크 오류 재시도, 데이터 검증 실패 처리, 복구 절차 +6. **Deployment**: "배포 방식, 운영 절차, 배포 환경을 정의하는가?" → YES면 Deployment + - 예: 컨테이너 배포, 스택 관리, 운영 가이드 +7. **Documentation**: "문서 작성, 가이드, 매뉴얼, 안내서를 정의하는가?" → YES면 Documentation + - 예: 사용자 가이드, API 문서, 운영 매뉴얼, 튜토리얼 +8. **Integration**: "외부 시스템 연동, 플러그인, 확장 기능을 정의하는가?" → YES면 Integration + - 예: 3rd party 서비스 연동, 플러그인 인터페이스, 확장 메커니즘 +9. **Migration**: "데이터 이전, 버전 업그레이드, 마이그레이션을 정의하는가?" → YES면 Migration + - 예: 데이터 마이그레이션, 버전 호환성, 업그레이드 절차 +10. **Testing**: "테스트 전략, QA 요구사항, 검증 방식을 정의하는가?" → YES면 Testing + - 예: 테스트 커버리지, 회귀 테스트, 수락 테스트 기준 +11. **Non-Functional**: 위 모든 경우에 해당하지 않으면 → Non-Functional + - 예: 성능 목표, 보안 정책, 확장성, 호환성, 신뢰성 요구사항 + +**주의**: requirement가 여러 분류에 해당하면 주된 제약 또는 목적에 따라 하나로만 선택한다. 예를 들어: +- "Docker Compose로 배포할 수 있어야 한다" → Deployment +- "배포 시 환경 변수로 설정 가능해야 한다" → Configuration +- "배포 전에 다음 점검 항목을 검증해야 한다" → Testing/Documentation + +- `docs/SRS/index.md`는 SRS 운영 기준과 Requirement Register를 관리하는 문서로 유지한다. +- `docs/SRS/REQ-XXX_*.md`는 하나의 requirement만 다루고, 다른 requirement 내용을 섞지 않는다. 파일은 Type별 폴더에 저장한다. 예: `docs/SRS/Deployment/REQ-001_docker-compose-release.md` +- 새 requirement 번호의 source of truth는 `docs/SRS/index.md`의 `Next Requirement ID`다. +- requirement 본문에는 최소한 `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log`가 포함되어야 한다. +- `Acceptance Criteria`는 관찰 가능하고 test 가능한 문장으로 작성한다. +- 모호한 표현("개선한다", "적절히 처리한다", "최적화한다")은 측정 기준 없이 사용하지 않는다. +- requirement를 수정하면 해당 REQ 파일의 `Change Log`도 함께 갱신한다. +- `Impacted Area`는 `Docs`, `Modules`, `Tests`를 모두 검토한 뒤 작성한다. 해당 항목이 없으면 `없음`으로 명시한다. +- implementation issue/PR를 생성하거나 병합할 때는 `docs/SRS/traceability-matrix.md`에서 해당 REQ 행의 `Issue/PR`, `Tests`, `Status`를 갱신한다. +- 새로운 Type으로 requirement를 추가할 때는 `docs/SRS//` 폴더 하위에 저장한다. 아직 없는 Type 폴더면 생성한다. +- 새로운 requirement를 추가하면 `docs/SRS/index.md`의 Requirement Register에도 같은 ID를 추가한다. +- 새로운 requirement를 추가하거나 Register가 바뀌면 `docs/SRS/index.md`의 `Next Requirement ID`도 함께 갱신한다. +- `docs/SRS/index.md`의 `Recent Change Log Summary`는 최신 5건만 유지하고 최신 항목을 위에 둔다. +- requirement ID와 파일명 prefix는 일치해야 한다. 예: `REQ-002`는 `docs/SRS//REQ-002_.md`. +- requirement 상태를 `Approved` 또는 `Implemented`로 바꿀 때는 acceptance criteria와 change history가 누락되지 않았는지 먼저 검토한다. +- requirement 상태를 전환할 때는 `docs/SRS/index.md`의 `Status Transition Evidence` 기준을 따른다. diff --git a/base/.github/prompts/approve-requirement.prompt.md b/base/.github/prompts/approve-requirement.prompt.md new file mode 100644 index 0000000..3bf3267 --- /dev/null +++ b/base/.github/prompts/approve-requirement.prompt.md @@ -0,0 +1,26 @@ +--- +description: "Use when: decide whether a Proposed requirement is ready to move to Approved and generate the required status-change edits" +name: "Approve Requirement" +argument-hint: "Paste a Proposed REQ file or select requirement text" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 requirement 문서가 `Proposed`에서 `Approved`로 전환 가능한지 검토한다. + +입력: +${input} + +검토 규칙: +- 기준 문서는 [SRS index](../../docs/SRS/index.md) 의 `Status Transition Evidence`와 해당 requirement 문서 구조다. +- `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log` 존재 여부를 검토한다. +- Type이 SRS index의 분류 도표에 정의된 타입 중 정확하게 하나로 지정되었는지 확인한다. +- `Acceptance Criteria`가 구현 기준으로 충분히 구체적인지 판단한다. +- 중요한 assumption 또는 open question이 남아 있으면 승인 blocker로 본다. +- 승인 가능하면 status 변경과 Change Log 추가 초안을 함께 제안한다. +- 승인 불가하면 blocker를 우선순위 순으로 정리한다. + +출력 형식: +1. Approval Decision: `Approved-ready` 또는 `Not ready` +2. Blockers +3. Suggested Edits +4. Approval Change Log Draft diff --git a/base/.github/prompts/complete-requirement.prompt.md b/base/.github/prompts/complete-requirement.prompt.md new file mode 100644 index 0000000..ea0de9a --- /dev/null +++ b/base/.github/prompts/complete-requirement.prompt.md @@ -0,0 +1,32 @@ +--- +description: "Use when: complete a partial SRS requirement draft into a full REQ file with acceptance criteria and impacted area" +name: "Complete Requirement Draft" +argument-hint: "Paste a partial requirement draft or describe the feature" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 입력을 기반으로 SRS requirement 초안을 완성한다. + +입력: +${input} + +작업 규칙: +- SRS 기준 문서는 [SRS index](../../docs/SRS/index.md) 와 관련 [REQ template](../../docs/SRS/REQ-template.md) 를 따른다. +- 입력에 `REQ-XXX` 형식의 명시적 ID가 없으면, [SRS index](../../docs/SRS/index.md)의 `Next Requirement ID`를 우선 읽어 그 값을 사용한다. `Next Requirement ID`가 없거나 신뢰할 수 없으면 `Requirement Register`와 기존 REQ 파일들 기준의 최고 번호에 1을 더한 값을 사용한다. +- 사용자가 이미 특정 REQ ID를 명시했으면 그 ID를 유지하고 임의로 바꾸지 않는다. +- requirement의 유형을 인식하고 `Type` 필드를 정확하게 지정한다. 분류는 [SRS index](../../docs/SRS/index.md)의 "Requirement 작성 규칙" 또는 [SRS instructions](../../.github/instructions/srs.instructions.md)의 "분류 의사결정 규칙"을 따른다. +- 최종 `Type`과 `REQ ID`를 이용해 추천 파일 경로를 함께 제시한다. 형식은 `docs/SRS//REQ-XXX_.md`를 사용한다. +- ``는 requirement 제목을 기반으로 소문자 kebab-case 영문으로 생성한다. 공백은 `-`로 치환하고, 영문/숫자/`-`만 남긴다. +- 제목이 한글이거나 slug 생성이 불명확하면 의미를 유지하는 짧은 영문 slug를 보수적으로 제안한다. +- 사용자가 제공한 의도와 핵심 동작을 보존한다. +- 빠진 항목은 보수적으로 보완하되, 추정이 필요한 내용은 `Notes`에 assumption으로 분리한다. +- `Acceptance Criteria`는 반드시 test 가능하게 작성한다. +- `Impacted Area`는 Docs, Modules, Tests로 구분한다. +- 새로 할당한 ID를 문서 제목 `# REQ-XXX: ...`에 반영한다. +- 결과는 REQ 파일에 바로 붙여넣을 수 있는 Markdown 형식으로만 출력한다. +- 코드 구현 계획은 포함하지 않는다. + +출력 형식: +1. `Recommended File Path: docs/SRS//REQ-XXX_.md` 한 줄 +2. 실제 할당된 ID가 반영된 `# REQ-XXX: ...` 형식의 전체 문서 본문 +3. `## Change Log` 아래에 초안 생성용 `CHG-YYYYMMDD-01` 항목 1개 포함 diff --git a/base/.github/prompts/create-implementation-issue.prompt.md b/base/.github/prompts/create-implementation-issue.prompt.md new file mode 100644 index 0000000..1566340 --- /dev/null +++ b/base/.github/prompts/create-implementation-issue.prompt.md @@ -0,0 +1,42 @@ +--- +description: "Use when: create an implementation plan and GitHub issue draft from an Approved requirement" +name: "Create Implementation Issue" +argument-hint: "Paste an Approved REQ file" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 `Approved` requirement 문서를 기반으로 implementation issue 초안을 생성한다. + +입력: +${input} + +작업 규칙: +- 기준 문서는 [SRS index](../../docs/SRS/index.md) 와 해당 REQ 문서다. +- 출력 언어는 기본적으로 한국어로 작성한다. +- section 제목, 설명, 작업 지시문은 한국어를 우선 사용하되, 중요한 technical term는 영어를 유지하거나 한국어 뒤 괄호로 병기한다. 예: `AI Agent Work`, `Human Verification Checklist`, `Acceptance Criteria`, `Open Questions`, `Risk`, `Definition of Done`. +- 코드 식별자, 파일 경로, 명령어, API 이름, 테스트 이름, UI 라벨 등 구현 대상과 직접 연결되는 literal은 영어 원문을 유지한다. +- requirement의 `Acceptance Criteria`를 구현 task로 분해한다. +- task는 구현 단위로 나누고, 각 task를 `AI Agent가 완료 가능한 작업`과 `Human이 수행해야 하는 작업`으로 구분한다. +- test task를 반드시 포함한다. +- 각 acceptance criteria 또는 task에 대해 다음을 판단한다: + - AI Agent가 코드 변경과 자동 검증까지 완료할 수 있는가 + - Human이 직접 확인해야 하는 수동 검증이 남는가 +- Human이 직접 확인해야 하는 항목이 있으면 `Human Verification Checklist`에 구체적인 확인 절차와 기대 결과를 작성한다. +- AI Agent가 자체적으로 테스트를 완료할 수 있어 Human 확인이 불필요하면 `Human Verification Checklist`에 `No manual verification required.`를 명시하고 그 근거를 `AI Validation Summary`에 적는다. +- `Impacted Area`를 기반으로 module/docs/test 변경 범위를 제시한다. +- 미해결 가정이나 리스크가 있으면 issue 본문에 `Open Questions`와 `Risks`로 명시한다. +- 출력은 GitHub issue에 바로 붙여 넣을 수 있는 Markdown 형식으로만 작성한다. + +출력 형식: +1. Issue Title +2. 배경 (Context) +3. 범위 (Scope) +4. AI Agent 작업 (AI Agent Work) 체크리스트 +5. 사람 작업 (Human Work) 체크리스트 +6. Acceptance Criteria Mapping (REQ 기준) +7. AI 검증 요약 (AI Validation Summary) +8. 사람 확인 체크리스트 (Human Verification Checklist) +9. 열린 질문 (Open Questions) +10. 리스크 (Risks) +11. 완료 조건 (Definition of Done) +12. GitHub Issue Create Command (`gh issue create`) 예시 diff --git a/base/.github/prompts/decide-requirement-scope.prompt.md b/base/.github/prompts/decide-requirement-scope.prompt.md new file mode 100644 index 0000000..4a847ec --- /dev/null +++ b/base/.github/prompts/decide-requirement-scope.prompt.md @@ -0,0 +1,38 @@ +--- +description: "Use when: evaluate whether a new feature or change request needs a new SRS requirement, an update to an existing one, a technical spec update only, or owner decision — before starting requirement drafting" +name: "Decide Requirement Scope" +argument-hint: "Describe the feature or change request: include feature description, issue body, and/or PR/diff summary" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 입력을 기반으로, 이 변경이 SRS 대상인지 아닌지를 판정한다. + +입력: +${input} + +판정 규칙: +- 기준 문서는 [requirement-scope-decision instructions](../../.github/instructions/requirement-scope-decision.instructions.md) 와 [SRS index](../../docs/SRS/index.md) 의 Requirement Register다. +- 다음 4가지 결과 중 하나를 반드시 반환한다. + 1. `New SRS requirement needed` + 2. `Update existing SRS requirement` + 3. `Technical spec only` + 4. `Needs owner decision` +- 판정 기준을 순서대로 적용한다. + - 사용자/외부 시스템이 관찰하는 계약(API, CLI, UI, Event, Data schema, Config, Deployment, Integration)이 바뀌는가? → YES면 SRS + - 측정 가능한 Non-Functional requirement(성능, 보안, 가용성 등)가 추가/변경되는가? → YES면 SRS + - Acceptance Criteria 및 명시적 승인 게이트가 필요한가? → YES면 SRS + - 위 기준에 모두 해당하지 않으면서 내부 리팩터링·라이브러리 교체·코드 구조 변경·구현 최적화인가? → Technical spec only + - 위 기준만으로 분류가 확정되지 않거나 운영 책임/리스크가 불명확한가? → Needs owner decision +- SRS 대상으로 판정되면, 신규 기능/계약인지 기존 REQ 범위 내 변경인지를 구분한다. + - 기존 REQ에 해당하는 경우 [SRS index](../../docs/SRS/index.md) 의 Requirement Register에서 후보 REQ ID를 찾아 제시한다. +- Technical spec only로 판정되면, 불필요한 REQ 생성을 명시적으로 피하도록 안내한다. +- 결과에는 항상 `Why`와 `Next Action`을 포함한다. +- `Open Questions`가 없으면 `없음`으로 명시한다. + +출력 형식: +1. **Decision**: `New SRS requirement needed` / `Update existing SRS requirement` / `Technical spec only` / `Needs owner decision` 중 하나 +2. **Why**: 판정 근거 — 어떤 기준에 해당하는지 구체적으로 설명 +3. **Recommended Type**: (SRS 대상인 경우만) Interface / Data / Deployment / Configuration 등 해당 requirement type +4. **Existing REQ candidates**: (Update 가능성이 있는 경우만) 관련 REQ ID와 제목 목록. 해당 없으면 생략 +5. **Next Action**: 판정 결과에 따른 다음 단계 안내 +6. **Open Questions**: 추가 확인이 필요한 사항. 없으면 `없음` diff --git a/base/.github/prompts/mark-requirement-implemented.prompt.md b/base/.github/prompts/mark-requirement-implemented.prompt.md new file mode 100644 index 0000000..b1567f5 --- /dev/null +++ b/base/.github/prompts/mark-requirement-implemented.prompt.md @@ -0,0 +1,25 @@ +--- +description: "Use when: decide whether an Approved requirement is ready to move to Implemented and generate the required status-change edits" +name: "Mark Requirement Implemented" +argument-hint: "Paste an Approved REQ file and implementation evidence" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 requirement 문서와 implementation evidence를 검토해 `Approved`에서 `Implemented`로 전환 가능한지 판단한다. + +입력: +${input} + +검토 규칙: +- 기준 문서는 [SRS index](../../docs/SRS/index.md) 의 `Status Transition Evidence`와 해당 requirement 문서 구조다. +- requirement의 `Acceptance Criteria`가 implementation evidence, tests, 또는 manual verification으로 충족되었는지 확인한다. +- 충족되지 않은 acceptance criteria가 있으면 구현 완료로 판정하지 않는다. +- 구현 완료 가능하면 status 변경, 잔여 리스크, Change Log 추가 초안을 함께 제안한다. +- 구현 완료 불가하면 미충족 기준과 필요한 검증 항목을 정리한다. + +출력 형식: +1. Implementation Decision: `Implemented-ready` 또는 `Not ready` +2. Unverified Criteria +3. Suggested Edits +4. Implementation Change Log Draft +5. Residual Risk diff --git a/base/.github/prompts/reopen-requirement-status.prompt.md b/base/.github/prompts/reopen-requirement-status.prompt.md new file mode 100644 index 0000000..695c628 --- /dev/null +++ b/base/.github/prompts/reopen-requirement-status.prompt.md @@ -0,0 +1,44 @@ +--- +description: "Use when: move Approved, Implemented, or Deprecated requirements back to Proposed (or Re-review) with decision checkpoints, conflict candidates, and fallback REQ discovery" +name: "Reopen Requirement Status" +argument-hint: "(선택) REQ-XXX, REQ-XXX; 변경 의도와 범위 설명" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 입력을 기준으로 지정된 requirement를 재검토 상태로 되돌리는 변경안을 만든다. + +입력: +${input} + +입력 해석 규칙: +- 입력 형식은 기본적으로 `REQ-XXX, REQ-XXX; 이렇게 이렇게 변경할거야.` 를 따른다. +- `;` 앞은 대상 REQ ID 목록, 뒤는 변경 의도/배경으로 해석한다. +- 구분자는 쉼표/공백을 모두 허용하되, 최종적으로 중복 없는 REQ ID 목록으로 정규화한다. +- 입력에 REQ ID가 없으면 전체 입력을 변경 의도/배경으로 간주한다. + +작업 규칙: +- 기준 문서는 `docs/SRS/index.md`, 해당 REQ 파일, [SRS instructions](../../.github/instructions/srs.instructions.md), [Reopen requirement discovery instructions](../../.github/instructions/reopen-requirement-discovery.instructions.md) 다. +- 대상 REQ ID가 명시되지 않으면 `docs/SRS/index.md` Requirement Register와 REQ 파일 제목/Intent/Requirement/Impacted Area를 기반으로 관련 후보를 탐색한다. +- 후보 탐색, 점수화, 신뢰도, 동점 처리, 도메인 키워드 정규화는 [Reopen requirement discovery instructions](../../.github/instructions/reopen-requirement-discovery.instructions.md)를 그대로 따른다. +- REQ 미지정 시 후보는 최대 5개까지 제시하고, 신뢰도(High/Medium/Low)와 근거(키워드/영향 영역)를 함께 제시한다. +- REQ 미지정 시 확정 전에는 상태 변경안을 단정하지 말고, `Immediate Questions`에서 최종 대상 REQ 확인 질문을 먼저 제시한다. +- 사용자가 후보 중 선택하면 선택된 REQ 기준으로 상태 되돌림 변경안을 완성한다. +- 대상 REQ의 현재 상태가 `Approved`, `Implemented`, `Deprecated` 중 하나인지 확인한다. +- 상태 되돌림의 목표 상태는 기본 `Proposed`로 제안한다. +- 팀이 `재검토 상태`를 별도로 운용하면 목표 상태를 `Re-review`(또는 팀 표기)로 제안할 수 있다. 단, 최종 상태값은 사용자 결정 항목으로 분리한다. +- 각 REQ마다 왜 되돌려야 하는지(계약 변경, acceptance criteria 변경, 리스크 발견, 구현 불일치 등) 근거를 1-2줄로 요약한다. +- 변경 전/후 영향으로 충돌 가능성이 있는 REQ 후보를 `docs/SRS/index.md` Requirement Register 기준으로 제시한다. +- 충돌 후보는 다음 기준으로 찾는다: 동일 기능 영역, 동일 인터페이스/API/CLI/데이터 스키마, 같은 모듈/문서 영향 범위, 기존 acceptance criteria 중복/상충. +- 사용자가 결정해야 할 항목을 별도 섹션으로 분리한다. +- 결정 누락 시 즉시 물어야 할 질문을 먼저 제시한다. +- 실제 문서 변경을 수행하는 대신, 바로 적용 가능한 수정안(상태, Change Log, impacted area 재검토 포인트)을 제시한다. + +출력 형식: +1. Reopen Decision +2. Immediate Questions (없으면 `없음`) +3. Detected REQ Candidates (REQ 미지정인 경우 필수, 지정된 경우 생략 가능) +4. User Decisions Required +5. Suggested Status Changes (REQ별: `from -> to`) +6. Conflict Candidate REQs (REQ별 후보 ID와 충돌 이유) +7. Suggested REQ Edits (Status, Intent/Requirement/Acceptance Criteria 영향, Change Log 초안) +8. Next Action diff --git a/base/.github/prompts/sync-srs-index.prompt.md b/base/.github/prompts/sync-srs-index.prompt.md new file mode 100644 index 0000000..e8ecc03 --- /dev/null +++ b/base/.github/prompts/sync-srs-index.prompt.md @@ -0,0 +1,28 @@ +--- +description: "Use when: update docs/SRS/index.md Requirement Register or Recent Change Log Summary after adding or editing REQ files" +name: "Sync SRS Index" +argument-hint: "Paste changed REQ files or describe what changed in SRS" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 입력을 기준으로 `docs/SRS/index.md`를 갱신하기 위한 제안을 만든다. + +입력: +${input} + +작업 규칙: +- 기준 문서는 [SRS index](../../docs/SRS/index.md) 이다. +- 새 requirement가 추가되었거나 Register가 바뀌면 `Requirement Register`의 최고 번호를 기준으로 다음 번호를 계산해 `Next Requirement ID`도 함께 갱신한다. +- 새 requirement가 추가되었으면 `Requirement Register`에 들어갈 행을 만든다. +- 기존 requirement가 수정되었으면 `Recent Change Log Summary`에 들어갈 요약 항목을 만든다. +- 기존 구조를 바꾸지 말고, 필요한 행과 요약만 제안한다. +- `Next Requirement ID`는 `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 큰 값이어야 한다. +- `Recent Change Log Summary`는 최신 변경을 위에 두고, 짧은 요약 1줄로 작성한다. +- `Recent Change Log Summary`는 최신 5건만 유지하도록 제안한다. 5건을 초과하면 제거 또는 대체해야 할 가장 오래된 항목도 함께 설명한다. +- 출력은 사람이 `docs/SRS/index.md`에 바로 반영할 수 있는 Markdown 조각으로만 작성한다. + +출력 형식: +1. `Next Requirement ID`에 반영할 Markdown bullet +2. `Requirement Register`에 추가 또는 수정할 Markdown table row +3. `Recent Change Log Summary`에 추가할 Markdown section +4. 필요한 경우 only, index에서 제거 또는 갱신해야 할 기존 항목 설명 diff --git a/base/.github/prompts/sync-technical-specs.prompt.md b/base/.github/prompts/sync-technical-specs.prompt.md new file mode 100644 index 0000000..3cdb1c6 --- /dev/null +++ b/base/.github/prompts/sync-technical-specs.prompt.md @@ -0,0 +1,34 @@ +--- +description: "Use when: verify one or more REQ IDs are Approved and create or update the matching docs/technical specifications" +name: "Sync Technical Specs" +argument-hint: "Enter REQ IDs, e.g. REQ-011, REQ-007, REQ-012" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 입력으로 지정된 requirement ID 목록에 대해, 승인 상태를 확인하고 해당하는 `docs/technical/` 문서를 실제로 갱신한다. + +입력: +${input} + +작업 규칙: +- 입력에서 `REQ-XXX` 목록을 추출한다. 쉼표 또는 공백으로 구분된 여러 ID를 지원한다. +- 먼저 `docs/SRS/index.md`의 `Requirement Register`와 각 REQ 파일의 `Status`를 함께 확인한다. +- 두 곳 모두 `Approved`인 requirement만 기술명세 업데이트 대상으로 포함한다. +- 하나라도 `Approved`가 아니거나 파일을 찾지 못하면 해당 REQ는 blocker로 보고하고, 그 REQ에 대한 technical doc은 수정하지 않는다. +- `docs/technical/index.md`와 각 technical doc 상단의 `Related REQ`를 기준으로 기존 연결 문서를 찾는다. +- 연결 문서가 있으면 현재 승인된 requirement에 맞게 내용을 실제로 업데이트한다. +- 연결 문서가 없으면 `docs/technical/` 아래에 새 문서를 생성하고 `docs/technical/index.md`에 항목을 추가한다. +- 기술 문서는 SRS를 source of truth로 따르며, 외부 계약이나 acceptance criteria를 재정의하지 않는다. 대신 구현 상세(모듈 책임, 파일 위치, 설정/환경값, 런타임 흐름, validation notes, open implementation points)를 정리한다. +- 문서 상단에는 반드시 `- Related REQ: ...`를 유지하거나 추가한다. +- `REQ-011`, `REQ-007`, `REQ-012`는 다음 문서를 우선 후보로 사용한다: + - `REQ-011` → `docs/technical/project-structure-baseline.md` + - `REQ-007` → `docs/technical/python-runtime-and-packaging.md` + - `REQ-012` → `docs/technical/settings-and-env-loading.md` +- `docs/technical/index.md`의 문서 목록과 연관 REQ 표도 문서 생성/연결 변경 사항에 맞게 함께 갱신한다. +- 실제 파일 변경을 수행한 뒤, 변경된 파일과 각 REQ의 승인 확인 결과를 요약한다. + +출력 형식: +1. Approval Check +2. Technical Spec Updates +3. Files Changed +4. Remaining Blockers or Notes diff --git a/base/.github/prompts/validate-requirement.prompt.md b/base/.github/prompts/validate-requirement.prompt.md new file mode 100644 index 0000000..bbb9ba1 --- /dev/null +++ b/base/.github/prompts/validate-requirement.prompt.md @@ -0,0 +1,24 @@ +--- +description: "Use when: review an SRS requirement for ambiguity, missing acceptance criteria, missing impacted area, and change-log gaps" +name: "Validate Requirement Quality" +argument-hint: "Paste a REQ file or select requirement text" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 requirement 문서를 검토한다. + +입력: +${input} + +검토 규칙: +- 기준은 [SRS index](../../docs/SRS/index.md) 와 관련 requirement 문서 구조를 따른다. +- 다음 항목을 우선 검토한다: 모호한 표현, test 불가능한 문장, 누락된 acceptance criteria, 누락된 impacted area, 상태/유형/우선순위/owner 누락, Type이 의사결정 규칙에 따라 정확하게 지정되었는지, Change Log 누락 또는 불일치. + - Type 분류 규칙: 사용자 조작 방식 → Interface, 데이터 저장/구조 → Data, 오류 처리 → Exception & Error Handling, 그 외 → Non-Functional +- 중요한 문제부터 우선순위 순으로 정리한다. +- 문제가 없으면 `No critical findings.` 로 시작한다. +- requirement 본문을 직접 다시 작성하지 말고, 수정 제안 위주로 답한다. + +출력 형식: +1. Findings +2. Open Questions +3. Suggested Edits diff --git a/base/.github/pull_request_template.md b/base/.github/pull_request_template.md new file mode 100644 index 0000000..4af7096 --- /dev/null +++ b/base/.github/pull_request_template.md @@ -0,0 +1,24 @@ +## Requirement Traceability + +- Requirement ID: REQ-XXX +- Requirement Link: docs/SRS//REQ-XXX_.md +- Related Issue: # + +## Acceptance Criteria Coverage +- [ ] AC1: +- [ ] AC2: + +## Test Evidence +- Automated: + - +- Manual: + - + - If not required: `No manual verification required.` + +## Impacted Area Check +- Docs: +- Modules: +- Tests: + +## Release Notes Input +- [ ] Added REQ ID to release notes (or tracked release note draft) diff --git a/bootstrap/install.sh b/bootstrap/install.sh new file mode 100755 index 0000000..2cf7999 --- /dev/null +++ b/bootstrap/install.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +KIT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +MERGE_COPILOT_INSTRUCTIONS=0 +DRY_RUN=0 +PROJECT_ROOT="" + +usage() { + cat <<'EOF' +Usage: bootstrap/install.sh [PROJECT_ROOT] [--merge-copilot-instructions] [--dry-run] + +Options: + --merge-copilot-instructions Opt-in: safely merge .github/copilot-instructions.md + --dry-run Print planned changes without modifying files + -h, --help Show this help message +EOF +} + +log_action() { + local message="$1" + if [[ "$DRY_RUN" -eq 1 ]]; then + echo "[dry-run] $message" + else + echo "$message" + fi +} + +run_or_dry() { + if [[ "$DRY_RUN" -eq 1 ]]; then + log_action "$*" + return 0 + fi + "$@" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --merge-copilot-instructions) + MERGE_COPILOT_INSTRUCTIONS=1 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + echo "Unknown option: $1" >&2 + usage >&2 + exit 1 + ;; + *) + if [[ -z "$PROJECT_ROOT" ]]; then + PROJECT_ROOT="$1" + shift + else + echo "Unexpected extra argument: $1" >&2 + usage >&2 + exit 1 + fi + ;; + esac +done + +PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}" + +MANAGED_BEGIN='' +MANAGED_END='' +PROJECT_BEGIN='' +PROJECT_END='' + +count_occurrence() { + local needle="$1" + local file="$2" + grep -cF "$needle" "$file" || true +} + +build_managed_block() { + local kit_file="$1" + { + printf '%s\n' "$MANAGED_BEGIN" + cat "$kit_file" + printf '\n%s\n' "$MANAGED_END" + } +} + +append_project_section_if_missing() { + local file="$1" + local project_begin_count + local project_end_count + + project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$file")" + project_end_count="$(count_occurrence "$PROJECT_END" "$file")" + + if [[ "$project_begin_count" -eq 0 && "$project_end_count" -eq 0 ]]; then + cat >> "$file" < +$PROJECT_END +EOF + elif [[ "$project_begin_count" -ne "$project_end_count" ]]; then + return 1 + fi +} + +validate_merged_copilot_file() { + local file="$1" + local managed_begin_count + local managed_end_count + local project_begin_count + local project_end_count + + managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$file")" + managed_end_count="$(count_occurrence "$MANAGED_END" "$file")" + project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$file")" + project_end_count="$(count_occurrence "$PROJECT_END" "$file")" + + [[ "$managed_begin_count" -eq 1 ]] || return 1 + [[ "$managed_end_count" -eq 1 ]] || return 1 + [[ "$project_begin_count" -eq 1 ]] || return 1 + [[ "$project_end_count" -eq 1 ]] || return 1 +} + +merge_copilot_instructions() { + local kit_file="$KIT_ROOT/base/.github/copilot-instructions.md" + local host_file="$PROJECT_ROOT/.github/copilot-instructions.md" + local backup_file="$host_file.bak" + local tmp_file + local managed_block + local managed_begin_count + local managed_end_count + + if [[ "$MERGE_COPILOT_INSTRUCTIONS" -ne 1 ]]; then + return 0 + fi + + if [[ ! -f "$kit_file" ]]; then + log_action "Skipped Copilot merge: kit template not found at $kit_file" + return 0 + fi + + if [[ "$DRY_RUN" -eq 1 ]]; then + if [[ ! -f "$host_file" ]]; then + log_action "Would create and initialize $host_file with managed + project-specific sections" + return 0 + fi + + managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$host_file")" + managed_end_count="$(count_occurrence "$MANAGED_END" "$host_file")" + + log_action "Would back up $host_file to $host_file.bak" + if [[ "$managed_begin_count" -eq 1 && "$managed_end_count" -eq 1 ]]; then + log_action "Would update managed myagent-kit section in $host_file" + elif [[ "$managed_begin_count" -eq 0 && "$managed_end_count" -eq 0 ]]; then + log_action "Would append managed myagent-kit section to $host_file" + else + log_action "Would fail: malformed managed marker pair in $host_file" + return 1 + fi + + local project_begin_count + local project_end_count + project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$host_file")" + project_end_count="$(count_occurrence "$PROJECT_END" "$host_file")" + if [[ "$project_begin_count" -eq 0 && "$project_end_count" -eq 0 ]]; then + log_action "Would append project-specific editable section to $host_file" + elif [[ "$project_begin_count" -ne "$project_end_count" ]]; then + log_action "Would fail: malformed project-specific marker pair in $host_file" + return 1 + fi + return 0 + fi + + managed_block="$(build_managed_block "$kit_file")" + tmp_file="$(mktemp)" + + if [[ ! -f "$host_file" ]]; then + { + printf '%s\n' "$managed_block" + printf '\n%s\n' "$PROJECT_BEGIN" + printf '\n' + printf '%s\n' "$PROJECT_END" + } > "$host_file" + validate_merged_copilot_file "$host_file" + log_action "Created and initialized $host_file" + rm -f "$tmp_file" + return 0 + fi + + cp "$host_file" "$backup_file" + managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$host_file")" + managed_end_count="$(count_occurrence "$MANAGED_END" "$host_file")" + + if [[ "$managed_begin_count" -eq 1 && "$managed_end_count" -eq 1 ]]; then + awk \ + -v begin="$MANAGED_BEGIN" \ + -v end="$MANAGED_END" \ + -v block="$managed_block" ' + BEGIN { + in_block = 0 + replaced = 0 + } + $0 == begin { + if (!replaced) { + print block + replaced = 1 + } + in_block = 1 + next + } + $0 == end { + in_block = 0 + next + } + !in_block { + print + } + END { + if (!replaced) { + print "" + print block + } + } + ' "$host_file" > "$tmp_file" + elif [[ "$managed_begin_count" -eq 0 && "$managed_end_count" -eq 0 ]]; then + { + cat "$host_file" + printf '\n%s\n' "$managed_block" + } > "$tmp_file" + else + echo "Malformed managed marker pair in $host_file" >&2 + cp "$backup_file" "$host_file" + rm -f "$tmp_file" + return 1 + fi + + mv "$tmp_file" "$host_file" + append_project_section_if_missing "$host_file" + validate_merged_copilot_file "$host_file" + log_action "Merged $host_file (backup: $backup_file)" +} + +run_or_dry mkdir -p "$PROJECT_ROOT/.github/prompts" \ + "$PROJECT_ROOT/.github/instructions" \ + "$PROJECT_ROOT/.github/ISSUE_TEMPLATE" + +run_or_dry cp "$KIT_ROOT/base/.github/prompts/"*.prompt.md "$PROJECT_ROOT/.github/prompts/" +run_or_dry cp "$KIT_ROOT/base/.github/instructions/"*.instructions.md "$PROJECT_ROOT/.github/instructions/" +run_or_dry cp "$KIT_ROOT/base/.github/ISSUE_TEMPLATE/"*.md "$PROJECT_ROOT/.github/ISSUE_TEMPLATE/" +if [[ -f "$KIT_ROOT/base/.github/pull_request_template.md" ]]; then + run_or_dry cp "$KIT_ROOT/base/.github/pull_request_template.md" "$PROJECT_ROOT/.github/pull_request_template.md" +fi + +merge_copilot_instructions + +log_action "Installed myagent-kit assets into $PROJECT_ROOT/.github" diff --git a/templates/docs/SRS/REQ-template.md b/templates/docs/SRS/REQ-template.md new file mode 100644 index 0000000..863168c --- /dev/null +++ b/templates/docs/SRS/REQ-template.md @@ -0,0 +1,55 @@ +# REQ-XXX: <요구사항 제목> + +- Status: Proposed | Approved | Implemented | Deprecated +- Type: Interface | Data | Exception & Error Handling | Installation | Configuration | Deployment | Documentation | Integration | Testing | Non-Functional +- Priority: High | Medium | Low +- Owner: <담당자 또는 팀> + +## Intent +- <이 요구사항이 필요한 이유와 달성하려는 목표를 1~3문장으로 작성> + +## Requirement +- <시스템이 반드시 수행해야 하는 동작을 단정형 문장으로 작성> + +## Acceptance Criteria +1. Given <선행 조건>, When <행동>, Then <기대 결과> +2. Given <선행 조건>, When <행동>, Then <기대 결과> + +## Impacted Area +- Docs: <영향 받는 문서 경로> +- Modules: <영향 받는 모듈/컴포넌트> +- Tests: <추가/수정해야 하는 테스트> + +## Notes +- <가정, 제약, 미해결 이슈> + +## Change Log + +### CHG-YYYYMMDD-01 +- Date: YYYY-MM-DD +- Author: <작성자> + +#### Change Summary +- <무엇이 변경되었는지> + +#### Intent +- <왜 변경했는지> + +#### Acceptance Criteria Delta +- Added: + - <추가된 기준> +- Updated: + - <변경된 기준> +- Removed: + - <삭제된 기준> + +#### Impacted Area Delta +- Docs: + - <문서 영향> +- Modules: + - <코드 영향> +- Tests: + - <테스트 영향> + +#### Validation Plan +- <검증 방법과 완료 조건> diff --git a/templates/docs/SRS/TRACEABILITY-template.md b/templates/docs/SRS/TRACEABILITY-template.md new file mode 100644 index 0000000..9076648 --- /dev/null +++ b/templates/docs/SRS/TRACEABILITY-template.md @@ -0,0 +1,28 @@ +# SRS Traceability Matrix + + + +## Status Definition +- Planned: Requirement is defined but not yet implemented. +- In Progress: Implementation issue/PR exists and work is ongoing. +- Implemented: Code is merged and mapped acceptance criteria are complete. +- Verified: Required tests and manual checks are completed. +- Released: Included in a released version. + +## Matrix + +| REQ ID | AC Mapping | Requirement File | Implementation Issue | PR | Test Evidence | Requirement Status | Delivery Status | Last Updated | +|---|---|---|---|---|---|---|---|---| +| REQ-001 | AC1, AC2 | docs/SRS//REQ-001_.md | #123 | #456 | tests/... , CI run link | Approved | Planned | YYYY-MM-DD | + +## Update Rules +- REQ ID must exactly match the SRS requirement file prefix. +- AC Mapping should reference concrete IDs such as AC1, AC2. +- Test Evidence should point to executable artifacts: test file path, CI job, or manual verification note. +- Last Updated uses YYYY-MM-DD. +- If there is no value yet, use `TBD`. From e89690b1dd004296f16a0fcc3be528a0ee83435d Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Mon, 13 Apr 2026 23:26:54 +0900 Subject: [PATCH 04/21] draft: Reorganize documentation and update prompts --- ...open-requirement-discovery.instructions.md | 50 +++++ .../reopen-requirement-status.prompt.md | 44 +++++ .../prompts/sync-technical-specs.prompt.md | 34 ++++ .github/workflows/docs-sync-check.yml | 23 --- scripts/check_docs_sync.py | 177 ------------------ templates/docs/SRS/REQ-template.md | 55 ------ templates/docs/SRS/TRACEABILITY-template.md | 28 --- 7 files changed, 128 insertions(+), 283 deletions(-) create mode 100644 .github/instructions/reopen-requirement-discovery.instructions.md create mode 100644 .github/prompts/reopen-requirement-status.prompt.md create mode 100644 .github/prompts/sync-technical-specs.prompt.md delete mode 100644 .github/workflows/docs-sync-check.yml delete mode 100644 scripts/check_docs_sync.py delete mode 100644 templates/docs/SRS/REQ-template.md delete mode 100644 templates/docs/SRS/TRACEABILITY-template.md diff --git a/.github/instructions/reopen-requirement-discovery.instructions.md b/.github/instructions/reopen-requirement-discovery.instructions.md new file mode 100644 index 0000000..bb579df --- /dev/null +++ b/.github/instructions/reopen-requirement-discovery.instructions.md @@ -0,0 +1,50 @@ +--- +description: "Use when customizing REQ candidate discovery rules for /reopen-requirement-status command, including keyword dictionary, scoring, tie-breaks, and normalization policy." +applyTo: ".github/prompts/reopen-requirement-status.prompt.md" +--- +# Reopen Requirement Discovery Guidelines + +## 목적 +`/reopen-requirement-status` 입력에 REQ ID가 없을 때, 관련 REQ 후보를 일관된 기준으로 탐색하고 우선순위를 정한다. + +## 후보 탐색 우선순위 +1. 계약 키워드 일치 +- `API`, `endpoint`, `request`, `response`, `schema`, `field`, `CLI`, `command`, `option`, `UI`, `event`, `config`, `env`, `deploy`, `integration` +2. 영향 영역 일치 +- 입력의 변경 대상 모듈/문서/테스트와 REQ의 `Impacted Area` 교집합 +3. 목적/행동 일치 +- 입력의 동사/의도(추가, 제거, 변경, 완화, 강화, 비활성화 등)와 REQ `Intent`/`Requirement`의 의미 유사도 +4. 상태 가중치 +- 현재 상태가 `Approved`, `Implemented`, `Deprecated`인 REQ 우선 + +## 점수 및 신뢰도 +- 점수식: `키워드(0-5) + 영향영역(0-3) + 의미유사(0-2) + 상태가중치(0-1)` +- 정렬: 총점 내림차순 +- 신뢰도: + - `High(7-11)` + - `Medium(4-6)` + - `Low(0-3)` + +## 도메인 키워드 사전 (커스터마이즈 가능) +- Auth & Access: `auth`, `login`, `oauth`, `token`, `permission`, `rbac`, `role`, `scope` +- Billing & Subscription: `billing`, `invoice`, `payment`, `subscription`, `plan`, `quota`, `credit` +- Data & Migration: `schema`, `migration`, `backfill`, `etl`, `import`, `export`, `retention` +- API & Contract: `api`, `endpoint`, `request`, `response`, `versioning`, `webhook`, `payload` +- Runtime & Operations: `deploy`, `rollback`, `release`, `monitoring`, `alert`, `slo`, `incident` +- Configuration: `config`, `env`, `feature flag`, `toggle`, `secret`, `parameter` +- UX & Interface: `ui`, `ux`, `screen`, `form`, `flow`, `command`, `cli`, `option` +- Testing & Quality: `test`, `e2e`, `regression`, `coverage`, `validation`, `acceptance` + +## 정규화 규칙 +- 사전에 없는 표현이 입력에 등장하면 의미가 유사한 사전 키워드로 1회 정규화해 근거에 표기한다. +- 예: `권한` -> `permission`, `과금` -> `billing` + +## 동점 처리 +- 동일 점수 후보가 여러 개면 최신 Change Log 항목이 있는 REQ를 우선한다. +- 여전히 동점이면 현재 active 상태(`Approved`/`Implemented`) REQ를 우선한다. +- 추가 동점이면 REQ ID가 작은 항목을 우선한다. + +## 출력 권장 +- 후보는 최대 5개 제시 +- 각 후보에 `REQ ID`, `신뢰도`, `점수`, `근거 키워드`, `영향 영역`을 포함 +- 확정 전에는 `Immediate Questions`로 최종 대상 REQ 선택을 먼저 요청 diff --git a/.github/prompts/reopen-requirement-status.prompt.md b/.github/prompts/reopen-requirement-status.prompt.md new file mode 100644 index 0000000..695c628 --- /dev/null +++ b/.github/prompts/reopen-requirement-status.prompt.md @@ -0,0 +1,44 @@ +--- +description: "Use when: move Approved, Implemented, or Deprecated requirements back to Proposed (or Re-review) with decision checkpoints, conflict candidates, and fallback REQ discovery" +name: "Reopen Requirement Status" +argument-hint: "(선택) REQ-XXX, REQ-XXX; 변경 의도와 범위 설명" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 입력을 기준으로 지정된 requirement를 재검토 상태로 되돌리는 변경안을 만든다. + +입력: +${input} + +입력 해석 규칙: +- 입력 형식은 기본적으로 `REQ-XXX, REQ-XXX; 이렇게 이렇게 변경할거야.` 를 따른다. +- `;` 앞은 대상 REQ ID 목록, 뒤는 변경 의도/배경으로 해석한다. +- 구분자는 쉼표/공백을 모두 허용하되, 최종적으로 중복 없는 REQ ID 목록으로 정규화한다. +- 입력에 REQ ID가 없으면 전체 입력을 변경 의도/배경으로 간주한다. + +작업 규칙: +- 기준 문서는 `docs/SRS/index.md`, 해당 REQ 파일, [SRS instructions](../../.github/instructions/srs.instructions.md), [Reopen requirement discovery instructions](../../.github/instructions/reopen-requirement-discovery.instructions.md) 다. +- 대상 REQ ID가 명시되지 않으면 `docs/SRS/index.md` Requirement Register와 REQ 파일 제목/Intent/Requirement/Impacted Area를 기반으로 관련 후보를 탐색한다. +- 후보 탐색, 점수화, 신뢰도, 동점 처리, 도메인 키워드 정규화는 [Reopen requirement discovery instructions](../../.github/instructions/reopen-requirement-discovery.instructions.md)를 그대로 따른다. +- REQ 미지정 시 후보는 최대 5개까지 제시하고, 신뢰도(High/Medium/Low)와 근거(키워드/영향 영역)를 함께 제시한다. +- REQ 미지정 시 확정 전에는 상태 변경안을 단정하지 말고, `Immediate Questions`에서 최종 대상 REQ 확인 질문을 먼저 제시한다. +- 사용자가 후보 중 선택하면 선택된 REQ 기준으로 상태 되돌림 변경안을 완성한다. +- 대상 REQ의 현재 상태가 `Approved`, `Implemented`, `Deprecated` 중 하나인지 확인한다. +- 상태 되돌림의 목표 상태는 기본 `Proposed`로 제안한다. +- 팀이 `재검토 상태`를 별도로 운용하면 목표 상태를 `Re-review`(또는 팀 표기)로 제안할 수 있다. 단, 최종 상태값은 사용자 결정 항목으로 분리한다. +- 각 REQ마다 왜 되돌려야 하는지(계약 변경, acceptance criteria 변경, 리스크 발견, 구현 불일치 등) 근거를 1-2줄로 요약한다. +- 변경 전/후 영향으로 충돌 가능성이 있는 REQ 후보를 `docs/SRS/index.md` Requirement Register 기준으로 제시한다. +- 충돌 후보는 다음 기준으로 찾는다: 동일 기능 영역, 동일 인터페이스/API/CLI/데이터 스키마, 같은 모듈/문서 영향 범위, 기존 acceptance criteria 중복/상충. +- 사용자가 결정해야 할 항목을 별도 섹션으로 분리한다. +- 결정 누락 시 즉시 물어야 할 질문을 먼저 제시한다. +- 실제 문서 변경을 수행하는 대신, 바로 적용 가능한 수정안(상태, Change Log, impacted area 재검토 포인트)을 제시한다. + +출력 형식: +1. Reopen Decision +2. Immediate Questions (없으면 `없음`) +3. Detected REQ Candidates (REQ 미지정인 경우 필수, 지정된 경우 생략 가능) +4. User Decisions Required +5. Suggested Status Changes (REQ별: `from -> to`) +6. Conflict Candidate REQs (REQ별 후보 ID와 충돌 이유) +7. Suggested REQ Edits (Status, Intent/Requirement/Acceptance Criteria 영향, Change Log 초안) +8. Next Action diff --git a/.github/prompts/sync-technical-specs.prompt.md b/.github/prompts/sync-technical-specs.prompt.md new file mode 100644 index 0000000..3cdb1c6 --- /dev/null +++ b/.github/prompts/sync-technical-specs.prompt.md @@ -0,0 +1,34 @@ +--- +description: "Use when: verify one or more REQ IDs are Approved and create or update the matching docs/technical specifications" +name: "Sync Technical Specs" +argument-hint: "Enter REQ IDs, e.g. REQ-011, REQ-007, REQ-012" +agent: "agent" +model: "GPT-5 (copilot)" +--- +다음 입력으로 지정된 requirement ID 목록에 대해, 승인 상태를 확인하고 해당하는 `docs/technical/` 문서를 실제로 갱신한다. + +입력: +${input} + +작업 규칙: +- 입력에서 `REQ-XXX` 목록을 추출한다. 쉼표 또는 공백으로 구분된 여러 ID를 지원한다. +- 먼저 `docs/SRS/index.md`의 `Requirement Register`와 각 REQ 파일의 `Status`를 함께 확인한다. +- 두 곳 모두 `Approved`인 requirement만 기술명세 업데이트 대상으로 포함한다. +- 하나라도 `Approved`가 아니거나 파일을 찾지 못하면 해당 REQ는 blocker로 보고하고, 그 REQ에 대한 technical doc은 수정하지 않는다. +- `docs/technical/index.md`와 각 technical doc 상단의 `Related REQ`를 기준으로 기존 연결 문서를 찾는다. +- 연결 문서가 있으면 현재 승인된 requirement에 맞게 내용을 실제로 업데이트한다. +- 연결 문서가 없으면 `docs/technical/` 아래에 새 문서를 생성하고 `docs/technical/index.md`에 항목을 추가한다. +- 기술 문서는 SRS를 source of truth로 따르며, 외부 계약이나 acceptance criteria를 재정의하지 않는다. 대신 구현 상세(모듈 책임, 파일 위치, 설정/환경값, 런타임 흐름, validation notes, open implementation points)를 정리한다. +- 문서 상단에는 반드시 `- Related REQ: ...`를 유지하거나 추가한다. +- `REQ-011`, `REQ-007`, `REQ-012`는 다음 문서를 우선 후보로 사용한다: + - `REQ-011` → `docs/technical/project-structure-baseline.md` + - `REQ-007` → `docs/technical/python-runtime-and-packaging.md` + - `REQ-012` → `docs/technical/settings-and-env-loading.md` +- `docs/technical/index.md`의 문서 목록과 연관 REQ 표도 문서 생성/연결 변경 사항에 맞게 함께 갱신한다. +- 실제 파일 변경을 수행한 뒤, 변경된 파일과 각 REQ의 승인 확인 결과를 요약한다. + +출력 형식: +1. Approval Check +2. Technical Spec Updates +3. Files Changed +4. Remaining Blockers or Notes diff --git a/.github/workflows/docs-sync-check.yml b/.github/workflows/docs-sync-check.yml deleted file mode 100644 index 920218e..0000000 --- a/.github/workflows/docs-sync-check.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Docs Sync Check - -on: - pull_request: - -jobs: - docs-sync-check: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Verify docs sync - run: | - python3 scripts/check_docs_sync.py --against origin/${{ github.base_ref }} --head ${{ github.sha }} diff --git a/scripts/check_docs_sync.py b/scripts/check_docs_sync.py deleted file mode 100644 index 959f2a2..0000000 --- a/scripts/check_docs_sync.py +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env python3 -"""Guardrail check for implementation-to-spec synchronization. - -Rules: -- If files under app/ change, at least one docs/technical/ file must be part of the same change set. -- If files under app/api/ or app/schemas/ change, docs/technical/api-contracts.md must be part of the same change set. -- If files under migrations/versions/ change, docs/technical/persistence-and-migrations.md must be part of the same change set. -- If migrations/versions/ files change, revision IDs found in changed migration files - must appear in docs/technical/persistence-and-migrations.md. -""" - -from __future__ import annotations - -import argparse -import re -import subprocess -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -TECHNICAL_DOC_PREFIX = "docs/technical/" -DB_SPEC = "docs/technical/persistence-and-migrations.md" -API_EXAMPLES_SPEC = "docs/technical/api-contracts.md" -APP_PREFIX = "app/" -API_PREFIXES = ("app/api/", "app/schemas/") -MIGRATION_PREFIX = "migrations/versions/" - - -class CheckError(Exception): - pass - - -def _run_git(args: list[str]) -> str: - result = subprocess.run( - ["git", *args], - cwd=ROOT, - check=True, - capture_output=True, - text=True, - ) - return result.stdout - - -def _ref_exists(ref: str) -> bool: - result = subprocess.run( - ["git", "rev-parse", "--verify", "--quiet", ref], - cwd=ROOT, - check=False, - capture_output=True, - text=True, - ) - return result.returncode == 0 - - -def _get_changed_files(*, staged: bool, against: str | None, head: str) -> list[str]: - if staged: - output = _run_git(["diff", "--name-only", "--cached", "--diff-filter=ACMR"]) - elif against: - if not _ref_exists(against): - raise CheckError(f"base ref not found: {against}") - if not _ref_exists(head): - raise CheckError(f"head ref not found: {head}") - output = _run_git(["diff", "--name-only", "--diff-filter=ACMR", f"{against}...{head}"]) - else: - if not _ref_exists("HEAD~1"): - # For a repository with a single commit, there is nothing to compare against. - return [] - output = _run_git(["diff", "--name-only", "--diff-filter=ACMR", "HEAD~1", "HEAD"]) - return [line.strip() for line in output.splitlines() if line.strip()] - - -def _extract_revision_id(path: Path) -> str | None: - text = path.read_text(encoding="utf-8") - match = re.search(r'^revision\s*=\s*["\']([^"\']+)["\']', text, flags=re.MULTILINE) - return match.group(1) if match else None - - -def _collect_changed_revision_ids(changed_files: list[str]) -> list[str]: - revision_ids: list[str] = [] - for file in changed_files: - if not file.startswith(MIGRATION_PREFIX) or not file.endswith(".py"): - continue - migration_path = ROOT / file - if not migration_path.exists(): - continue - revision_id = _extract_revision_id(migration_path) - if revision_id: - revision_ids.append(revision_id) - return revision_ids - - -def run_check(*, staged: bool, against: str | None, head: str) -> int: - try: - changed_files = _get_changed_files(staged=staged, against=against, head=head) - except CheckError as exc: - print(f"docs-sync-check: {exc}") - return 2 - except subprocess.CalledProcessError as exc: - print("docs-sync-check: failed to read git diff") - print(exc.stderr.strip()) - return 2 - - if not changed_files: - print("docs-sync-check: no changes detected") - return 0 - - needs_general_spec = any(path.startswith(APP_PREFIX) for path in changed_files) - needs_api_examples = any(path.startswith(prefix) for prefix in API_PREFIXES for path in changed_files) - needs_db_spec = any(path.startswith(MIGRATION_PREFIX) for path in changed_files) - - errors: list[str] = [] - - if needs_general_spec and not any(path.startswith(TECHNICAL_DOC_PREFIX) for path in changed_files): - errors.append( - "Application code changed but no docs/technical/ document was updated in the same commit/PR." - ) - - if needs_api_examples and API_EXAMPLES_SPEC not in changed_files: - errors.append( - "API/schema code changed but docs/technical/api-contracts.md was not updated in the same commit/PR." - ) - - if needs_db_spec and DB_SPEC not in changed_files: - errors.append( - "Migration changed but docs/technical/persistence-and-migrations.md was not updated in the same commit/PR." - ) - - if needs_db_spec: - db_spec_path = ROOT / DB_SPEC - if not db_spec_path.exists(): - errors.append("docs/technical/persistence-and-migrations.md is missing.") - else: - db_spec_text = db_spec_path.read_text(encoding="utf-8") - changed_revisions = _collect_changed_revision_ids(changed_files) - missing_revisions = [rev for rev in changed_revisions if rev not in db_spec_text] - if missing_revisions: - errors.append( - "Migration revision IDs missing in docs/technical/persistence-and-migrations.md: " - + ", ".join(sorted(set(missing_revisions))) - ) - - if errors: - print("docs-sync-check: FAILED") - for error in errors: - print(f"- {error}") - print("\nChanged files:") - for file in changed_files: - print(f"- {file}") - return 1 - - print("docs-sync-check: OK") - return 0 - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Verify implementation and docs stay in sync.") - parser.add_argument( - "--staged", - action="store_true", - help="Check staged files (for pre-commit).", - ) - parser.add_argument( - "--against", - default=None, - help="Git base ref/sha to diff against (for CI).", - ) - parser.add_argument( - "--head", - default="HEAD", - help="Git head ref/sha used with --against.", - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - sys.exit(run_check(staged=args.staged, against=args.against, head=args.head)) diff --git a/templates/docs/SRS/REQ-template.md b/templates/docs/SRS/REQ-template.md deleted file mode 100644 index 863168c..0000000 --- a/templates/docs/SRS/REQ-template.md +++ /dev/null @@ -1,55 +0,0 @@ -# REQ-XXX: <요구사항 제목> - -- Status: Proposed | Approved | Implemented | Deprecated -- Type: Interface | Data | Exception & Error Handling | Installation | Configuration | Deployment | Documentation | Integration | Testing | Non-Functional -- Priority: High | Medium | Low -- Owner: <담당자 또는 팀> - -## Intent -- <이 요구사항이 필요한 이유와 달성하려는 목표를 1~3문장으로 작성> - -## Requirement -- <시스템이 반드시 수행해야 하는 동작을 단정형 문장으로 작성> - -## Acceptance Criteria -1. Given <선행 조건>, When <행동>, Then <기대 결과> -2. Given <선행 조건>, When <행동>, Then <기대 결과> - -## Impacted Area -- Docs: <영향 받는 문서 경로> -- Modules: <영향 받는 모듈/컴포넌트> -- Tests: <추가/수정해야 하는 테스트> - -## Notes -- <가정, 제약, 미해결 이슈> - -## Change Log - -### CHG-YYYYMMDD-01 -- Date: YYYY-MM-DD -- Author: <작성자> - -#### Change Summary -- <무엇이 변경되었는지> - -#### Intent -- <왜 변경했는지> - -#### Acceptance Criteria Delta -- Added: - - <추가된 기준> -- Updated: - - <변경된 기준> -- Removed: - - <삭제된 기준> - -#### Impacted Area Delta -- Docs: - - <문서 영향> -- Modules: - - <코드 영향> -- Tests: - - <테스트 영향> - -#### Validation Plan -- <검증 방법과 완료 조건> diff --git a/templates/docs/SRS/TRACEABILITY-template.md b/templates/docs/SRS/TRACEABILITY-template.md deleted file mode 100644 index 9076648..0000000 --- a/templates/docs/SRS/TRACEABILITY-template.md +++ /dev/null @@ -1,28 +0,0 @@ -# SRS Traceability Matrix - - - -## Status Definition -- Planned: Requirement is defined but not yet implemented. -- In Progress: Implementation issue/PR exists and work is ongoing. -- Implemented: Code is merged and mapped acceptance criteria are complete. -- Verified: Required tests and manual checks are completed. -- Released: Included in a released version. - -## Matrix - -| REQ ID | AC Mapping | Requirement File | Implementation Issue | PR | Test Evidence | Requirement Status | Delivery Status | Last Updated | -|---|---|---|---|---|---|---|---|---| -| REQ-001 | AC1, AC2 | docs/SRS//REQ-001_.md | #123 | #456 | tests/... , CI run link | Approved | Planned | YYYY-MM-DD | - -## Update Rules -- REQ ID must exactly match the SRS requirement file prefix. -- AC Mapping should reference concrete IDs such as AC1, AC2. -- Test Evidence should point to executable artifacts: test file path, CI job, or manual verification note. -- Last Updated uses YYYY-MM-DD. -- If there is no value yet, use `TBD`. From 9787cf31594473d98289102b5a711221ea0006c0 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 00:13:34 +0900 Subject: [PATCH 05/21] feat: add portfolio ownership model, auth guards, and migration --- README.md | 20 --- app/api/portfolios.py | 82 +++++++++-- app/domain/models.py | 1 + app/domain/services.py | 12 +- app/repositories/portfolio_repository.py | 10 +- app/repositories/postgres.py | 49 +++++-- app/schemas/portfolio.py | 1 + app/services/portfolio_service.py | 19 ++- docker-compose.local-test.yml | 50 +++++++ ...-004_portfolio-ownership-access-control.md | 74 ++++++++++ docs/SRS/index.md | 5 +- docs/SRS/traceability-matrix.md | 1 + docs/technical/api-contracts.md | 7 +- docs/technical/auth-and-token-lifecycle.md | 7 +- docs/technical/index.md | 2 + docs/technical/persistence-and-migrations.md | 15 +- .../portfolio-ownership-and-auth-design.md | 137 ++++++++++++++++++ .../20260413_0010_portfolio_owner_user_id.py | 97 +++++++++++++ tests/test_cli.py | 14 +- tests/test_domain_services.py | 12 +- tests/test_portfolio_comments.py | 5 +- tests/test_portfolio_ownership.py | 131 +++++++++++++++++ 22 files changed, 681 insertions(+), 70 deletions(-) create mode 100644 docker-compose.local-test.yml create mode 100644 docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md create mode 100644 docs/technical/portfolio-ownership-and-auth-design.md create mode 100644 migrations/versions/20260413_0010_portfolio_owner_user_id.py create mode 100644 tests/test_portfolio_ownership.py diff --git a/README.md b/README.md index 95fec15..ad00e9f 100644 --- a/README.md +++ b/README.md @@ -57,26 +57,6 @@ uvicorn app.main:app --reload pytest ``` -## Production Deployment (HTTPS with Caddy) - -For production, it is recommended to run PortfolioHub behind [Caddy](https://caddyserver.com/), which handles TLS certificate provisioning automatically. - -A minimal `Caddyfile`: - -``` -your-domain.com { - reverse_proxy localhost:8000 -} -``` - -See **[docs/technical/deployment-and-operations.md](docs/technical/deployment-and-operations.md)** for a full guide covering: - -- Caddy installation on Linux/Ubuntu -- CORS configuration (`PORTFOLIOHUB_ALLOWED_ORIGINS`) -- Running `uvicorn` as a systemd service -- Docker Compose setup with a Caddy sidecar container -- Firewall port configuration (80, 443) - ## CLI The repository now includes a `portfoliohub` CLI that mirrors the main portfolio flows from `jsbot-kis`. diff --git a/app/api/portfolios.py b/app/api/portfolios.py index 230384a..be392b5 100644 --- a/app/api/portfolios.py +++ b/app/api/portfolios.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, Response, status -from app.core.auth import Permission, authorize_request +from app.core.auth import AuthContext, Permission, authorize_request from app.core.config import Settings, get_settings from app.repositories.factory import get_portfolio_repository_from_settings from app.schemas.portfolio import ( @@ -41,6 +41,28 @@ def _close_repository(repository: object) -> None: repository.close() +def _ownership_required(settings: Settings) -> bool: + return settings.db_enabled + + +def _require_owner_context(auth_context: AuthContext) -> str: + if auth_context.user_id is None: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Ownership context is required") + return auth_context.user_id + + +def _ensure_portfolio_owned(repository: object, portfolio_id: str, auth_context: AuthContext) -> None: + if auth_context.permission == Permission.ADMIN: + return + owner_user_id = _require_owner_context(auth_context) + try: + portfolio = repository.get(portfolio_id) + except ValueError as exc: + _raise_portfolio_error(exc) + if portfolio.owner_user_id != owner_user_id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Portfolio ownership mismatch") + + @router.get("", response_model=list[PortfolioListItem]) def list_portfolios( request: Request, @@ -48,10 +70,13 @@ def list_portfolios( status_filter: str | None = None, settings: Settings = Depends(get_settings), ) -> list[PortfolioListItem]: - authorize_request(Permission.READ, request, settings) + auth_context = authorize_request(Permission.READ, request, settings) service, repository = _get_service(settings) try: - return service.list_portfolios(broker=broker, status=status_filter) + owner_user_id: str | None = None + if _ownership_required(settings) and auth_context.permission != Permission.ADMIN: + owner_user_id = _require_owner_context(auth_context) + return service.list_portfolios(broker=broker, status=status_filter, owner_user_id=owner_user_id) finally: _close_repository(repository) @@ -64,9 +89,11 @@ def get_portfolio( recent_trades: int = 5, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: - authorize_request(Permission.READ, request, settings) + auth_context = authorize_request(Permission.READ, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.get_portfolio_status(portfolio_id=portfolio_id, recent_trades=recent_trades) finally: _close_repository(repository) @@ -83,10 +110,13 @@ def create_portfolio( *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: - return service.create_portfolio(payload) + owner_user_id = auth_context.user_id or "system" + if _ownership_required(settings): + owner_user_id = _require_owner_context(auth_context) + return service.create_portfolio(payload, owner_user_id=owner_user_id) finally: _close_repository(repository) @@ -99,9 +129,11 @@ def update_portfolio( *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.update_portfolio(portfolio_id, payload) finally: _close_repository(repository) @@ -115,9 +147,11 @@ def buy_portfolio_asset( *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.buy(portfolio_id, payload) finally: _close_repository(repository) @@ -131,9 +165,11 @@ def sell_portfolio_asset( *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.sell(portfolio_id, payload) finally: _close_repository(repository) @@ -147,9 +183,11 @@ def invest_buffer( *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.invest(portfolio_id, payload) finally: _close_repository(repository) @@ -163,9 +201,11 @@ def transfer_buffer( *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.transfer(portfolio_id, payload) finally: _close_repository(repository) @@ -179,9 +219,11 @@ def reset_portfolio( *, settings: Settings = Depends(get_settings), ) -> PortfolioDetailResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.reset(portfolio_id, payload) finally: _close_repository(repository) @@ -216,9 +258,11 @@ def create_comment( *, settings: Settings = Depends(get_settings), ) -> CommentResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.create_comment(portfolio_id, payload) except ValueError as exc: _raise_portfolio_error(exc) @@ -233,9 +277,11 @@ def list_comments( *, settings: Settings = Depends(get_settings), ) -> list[CommentResponse]: - authorize_request(Permission.READ, request, settings) + auth_context = authorize_request(Permission.READ, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.list_comments(portfolio_id) except ValueError as exc: _raise_portfolio_error(exc) @@ -252,9 +298,11 @@ def update_comment( *, settings: Settings = Depends(get_settings), ) -> CommentResponse: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) return service.update_comment(portfolio_id, comment_id, payload) except ValueError as exc: _raise_portfolio_error(exc) @@ -270,9 +318,11 @@ def delete_comment( *, settings: Settings = Depends(get_settings), ) -> Response: - authorize_request(Permission.TRADE_WRITE, request, settings) + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) service, repository = _get_service(settings) try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) service.delete_comment(portfolio_id, comment_id) except ValueError as exc: _raise_portfolio_error(exc) diff --git a/app/domain/models.py b/app/domain/models.py index 8559438..d78e91a 100644 --- a/app/domain/models.py +++ b/app/domain/models.py @@ -72,6 +72,7 @@ class Portfolio: status: str name: str symbol: str + owner_user_id: str | None = None cash_balance: float = 0.0 asset_quantity: float = 0.0 price: float = 0.0 diff --git a/app/domain/services.py b/app/domain/services.py index 4bb94e5..6eecaab 100644 --- a/app/domain/services.py +++ b/app/domain/services.py @@ -27,12 +27,14 @@ class PortfolioDomainRepository(Protocol): status: str = "enabled", active_strategy_id: str | None = None, avabile_strategies: list[str] | None = None, + owner_user_id: str | None = None, initial_cash: float = 0.0, ) -> Portfolio: ... def create_portfolio( self, portfolio_id: str, *, + owner_user_id: str, broker: str, status: str, name: str, @@ -45,7 +47,13 @@ class PortfolioDomainRepository(Protocol): avabile_strategies: list[str] | None, ) -> Portfolio: ... def update_portfolio(self, portfolio_id: str, **fields: object) -> Portfolio: ... - def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: ... + def list_portfolios( + self, + *, + broker: str | None = None, + status: str | None = None, + owner_user_id: str | None = None, + ) -> list[Portfolio]: ... def get_strategy_set(self, id: str) -> StrategySet | None: ... def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: ... def save( @@ -103,6 +111,7 @@ def apply_buy( status=portfolio.status, name=portfolio.name, symbol=portfolio.symbol, + owner_user_id=portfolio.owner_user_id, cash_balance=float(portfolio.cash_balance) - total_cost, asset_quantity=float(portfolio.asset_quantity) + quantity, price=price, @@ -164,6 +173,7 @@ def apply_sell( status=portfolio.status, name=portfolio.name, symbol=portfolio.symbol, + owner_user_id=portfolio.owner_user_id, cash_balance=float(portfolio.cash_balance) + net_proceeds, asset_quantity=float(portfolio.asset_quantity) - quantity, price=price, diff --git a/app/repositories/portfolio_repository.py b/app/repositories/portfolio_repository.py index 4bdc36b..187fea4 100644 --- a/app/repositories/portfolio_repository.py +++ b/app/repositories/portfolio_repository.py @@ -91,6 +91,7 @@ class PortfolioRepository(Protocol): status: str = "enabled", active_strategy_id: str | None = None, avabile_strategies: list[str] | None = None, + owner_user_id: str | None = None, initial_cash: float = 0.0, ) -> Portfolio: ... @@ -98,6 +99,7 @@ class PortfolioRepository(Protocol): self, portfolio_id: str, *, + owner_user_id: str, broker: str, status: str, name: str, @@ -112,7 +114,13 @@ class PortfolioRepository(Protocol): def update_portfolio(self, portfolio_id: str, **fields: object) -> Portfolio: ... - def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: ... + def list_portfolios( + self, + *, + broker: str | None = None, + status: str | None = None, + owner_user_id: str | None = None, + ) -> list[Portfolio]: ... def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: ... diff --git a/app/repositories/postgres.py b/app/repositories/postgres.py index 4422097..4a54005 100644 --- a/app/repositories/postgres.py +++ b/app/repositories/postgres.py @@ -70,6 +70,7 @@ _SELECT_PORTFOLIO = """ profit_buffer, active_strategy_id, avabile_strategies, + owner_user_id, created_at, updated_at, last_trade_at @@ -217,6 +218,7 @@ class PostgresPortfolioRepository: status TEXT NOT NULL DEFAULT 'enabled', name TEXT NOT NULL, symbol TEXT NOT NULL, + owner_user_id TEXT, cash_balance DOUBLE PRECISION NOT NULL, asset_quantity DOUBLE PRECISION NOT NULL, asset_price DOUBLE PRECISION NOT NULL DEFAULT 0, @@ -230,6 +232,7 @@ class PostgresPortfolioRepository: """ ) cur.execute("ALTER TABLE portfolios ADD COLUMN IF NOT EXISTS name TEXT") + cur.execute("ALTER TABLE portfolios ADD COLUMN IF NOT EXISTS owner_user_id TEXT") cur.execute("UPDATE portfolios SET name = symbol WHERE name IS NULL OR name = ''") cur.execute("ALTER TABLE portfolios ALTER COLUMN name SET NOT NULL") cur.execute( @@ -363,9 +366,10 @@ class PostgresPortfolioRepository: profit_buffer=float(row[8]), active_strategy_id=row[9] if len(row) > 9 else None, avabile_strategies=avabile_strategies, - created_at=row[11] if len(row) > 11 else None, - updated_at=row[12] if len(row) > 12 else None, - last_trade_at=row[13] if len(row) > 13 else None, + owner_user_id=row[11] if len(row) > 11 else None, + created_at=row[12] if len(row) > 12 else None, + updated_at=row[13] if len(row) > 13 else None, + last_trade_at=row[14] if len(row) > 14 else None, ) @staticmethod @@ -726,6 +730,7 @@ class PostgresPortfolioRepository: status: str = "enabled", active_strategy_id: str | None = None, avabile_strategies: list[str] | None = None, + owner_user_id: str | None = None, initial_cash: float = 0.0, ) -> Portfolio: self.ensure_schema() @@ -734,10 +739,10 @@ class PostgresPortfolioRepository: """ INSERT INTO portfolios ( portfolio_id, broker, status, name, symbol, - cash_balance, asset_quantity, asset_price, profit_buffer, + owner_user_id, cash_balance, asset_quantity, asset_price, profit_buffer, active_strategy_id, avabile_strategies ) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (portfolio_id) DO NOTHING """, ( @@ -746,6 +751,7 @@ class PostgresPortfolioRepository: status, name or symbol, symbol, + owner_user_id, float(initial_cash), 0.0, 0.0, @@ -762,6 +768,7 @@ class PostgresPortfolioRepository: self, portfolio_id: str, *, + owner_user_id: str, broker: str, status: str, name: str, @@ -782,13 +789,13 @@ class PostgresPortfolioRepository: """ INSERT INTO portfolios ( portfolio_id, broker, status, name, symbol, - cash_balance, asset_quantity, asset_price, profit_buffer, + owner_user_id, cash_balance, asset_quantity, asset_price, profit_buffer, active_strategy_id, avabile_strategies - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING portfolio_id, broker, status, name, symbol, cash_balance, asset_quantity, asset_price, profit_buffer, - active_strategy_id, avabile_strategies, created_at, updated_at, last_trade_at + active_strategy_id, avabile_strategies, owner_user_id, created_at, updated_at, last_trade_at """, ( portfolio_id, @@ -796,6 +803,7 @@ class PostgresPortfolioRepository: status, name, symbol, + owner_user_id, float(cash_balance), float(asset_quantity), float(asset_price), @@ -842,14 +850,20 @@ class PostgresPortfolioRepository: RETURNING portfolio_id, broker, status, name, symbol, cash_balance, asset_quantity, asset_price, profit_buffer, - active_strategy_id, avabile_strategies, created_at, updated_at, last_trade_at + active_strategy_id, avabile_strategies, owner_user_id, created_at, updated_at, last_trade_at """, (*params, portfolio_id), ) row = cur.fetchone() return self._row_to_portfolio(row) - def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: + def list_portfolios( + self, + *, + broker: str | None = None, + status: str | None = None, + owner_user_id: str | None = None, + ) -> list[Portfolio]: self.ensure_schema() query = _SELECT_PORTFOLIO clauses: list[str] = [] @@ -860,6 +874,9 @@ class PostgresPortfolioRepository: if status is not None: clauses.append("status = %s") params.append(status) + if owner_user_id is not None: + clauses.append("owner_user_id = %s") + params.append(owner_user_id) if clauses: query += "WHERE " + " AND ".join(clauses) + " " query += "ORDER BY updated_at DESC, portfolio_id ASC" @@ -1041,9 +1058,9 @@ class PostgresPortfolioRepository: last_trade_at = NULL WHERE portfolio_id = %s RETURNING - portfolio_id, broker, status, symbol, + portfolio_id, broker, status, name, symbol, cash_balance, asset_quantity, asset_price, profit_buffer, - active_strategy_id, avabile_strategies, created_at, updated_at, last_trade_at + active_strategy_id, avabile_strategies, owner_user_id, created_at, updated_at, last_trade_at """, (portfolio_id,), ) @@ -1080,9 +1097,9 @@ class PostgresPortfolioRepository: updated_at = NOW() WHERE portfolio_id = %s RETURNING - portfolio_id, broker, status, symbol, + portfolio_id, broker, status, name, symbol, cash_balance, asset_quantity, asset_price, profit_buffer, - active_strategy_id, avabile_strategies, created_at, updated_at, last_trade_at + active_strategy_id, avabile_strategies, owner_user_id, created_at, updated_at, last_trade_at """, (updated_buffer, portfolio_id), ) @@ -1149,9 +1166,9 @@ class PostgresPortfolioRepository: updated_at = NOW() WHERE portfolio_id = %s RETURNING - portfolio_id, broker, status, symbol, + portfolio_id, broker, status, name, symbol, cash_balance, asset_quantity, asset_price, profit_buffer, - active_strategy_id, avabile_strategies, created_at, updated_at, last_trade_at + active_strategy_id, avabile_strategies, owner_user_id, created_at, updated_at, last_trade_at """, (cash, buf, portfolio_id), ) diff --git a/app/schemas/portfolio.py b/app/schemas/portfolio.py index 2f77f19..12fb573 100644 --- a/app/schemas/portfolio.py +++ b/app/schemas/portfolio.py @@ -45,6 +45,7 @@ class PortfolioSummary(BaseModel): status: str name: str symbol: str + owner_user_id: str | None = None cash_balance: float = 0.0 asset_quantity: float = 0.0 price: float = 0.0 diff --git a/app/services/portfolio_service.py b/app/services/portfolio_service.py index 7308781..3f04d6e 100644 --- a/app/services/portfolio_service.py +++ b/app/services/portfolio_service.py @@ -49,6 +49,7 @@ def _to_portfolio_summary(portfolio: Portfolio): status=portfolio.status, name=portfolio.name, symbol=portfolio.symbol, + owner_user_id=portfolio.owner_user_id, cash_balance=portfolio.cash_balance, asset_quantity=portfolio.asset_quantity, price=portfolio.price, @@ -137,8 +138,14 @@ class PortfolioService: def __init__(self, repository: PortfolioRepository) -> None: self._repository = repository - def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[PortfolioListItem]: - portfolios = self._repository.list_portfolios(broker=broker, status=status) + def list_portfolios( + self, + *, + broker: str | None = None, + status: str | None = None, + owner_user_id: str | None = None, + ) -> list[PortfolioListItem]: + portfolios = self._repository.list_portfolios(broker=broker, status=status, owner_user_id=owner_user_id) return [ PortfolioListItem( **_to_portfolio_summary(portfolio).model_dump(), @@ -151,7 +158,12 @@ class PortfolioService: status_result = get_portfolio_status(self._repository, portfolio_id, recent_trades=recent_trades) return _to_detail_response(status_result) - def create_portfolio(self, payload: CreatePortfolioRequest) -> PortfolioDetailResponse: + def create_portfolio( + self, + payload: CreatePortfolioRequest, + *, + owner_user_id: str = "system", + ) -> PortfolioDetailResponse: active_strategy_id = payload.active_strategy_id if active_strategy_id is None: strategy_set = self._repository.get_or_create_strategy_set( @@ -162,6 +174,7 @@ class PortfolioService: avabile_strategies = _merge_strategy_ids(active_strategy_id, payload.avabile_strategies) portfolio = self._repository.create_portfolio( payload.portfolio_id, + owner_user_id=owner_user_id, broker=payload.broker, status=payload.status, name=payload.name or payload.symbol, diff --git a/docker-compose.local-test.yml b/docker-compose.local-test.yml new file mode 100644 index 0000000..a2d82a1 --- /dev/null +++ b/docker-compose.local-test.yml @@ -0,0 +1,50 @@ +services: + timescaledb: + image: timescale/timescaledb:latest-pg16 + container_name: portfolio-hub-timescaledb + environment: + POSTGRES_DB: portfolio_hub + POSTGRES_USER: portfolio_hub_user + POSTGRES_PASSWORD: change-me + ports: + - "127.0.0.1:5432:5432" + volumes: + - timescaledb_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U portfolio_hub_user -d portfolio_hub"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: unless-stopped + + redis-stack: + image: redis/redis-stack:latest + container_name: portfolio-hub-redis-stack + ports: + - "127.0.0.1:6379:6379" + - "127.0.0.1:8001:8001" + volumes: + - redis_stack_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + restart: unless-stopped + + loki: + image: grafana/loki:2.9.8 + container_name: portfolio-hub-loki + command: -config.file=/etc/loki/local-config.yaml + ports: + - "127.0.0.1:3100:3100" + volumes: + - loki_data:/loki + restart: unless-stopped + +volumes: + timescaledb_data: + redis_stack_data: + loki_data: diff --git a/docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md b/docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md new file mode 100644 index 0000000..a90be43 --- /dev/null +++ b/docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md @@ -0,0 +1,74 @@ +# REQ-004: 포트폴리오 소유주 기반 접근 제어 + +- Status: Proposed +- Type: Interface +- Priority: High +- Owner: Justin + +## Intent +- 시스템은 각 포트폴리오의 소유주를 명시적으로 저장하고, 인증된 주체의 권한과 소유권을 함께 평가해 접근 제어를 수행해야 한다. +- 동일 서버에서 운영되는 frontend와 portfolio-hub가 공통 인증 기반을 공유하더라도, 포트폴리오 단위 데이터 격리가 보장되도록 한다. + +## Requirement +- 시스템은 각 `portfolio` 레코드에 소유주 식별자 `owner_user_id`를 저장해야 한다. +- 시스템은 신규 포트폴리오 생성 시 요청 본문으로 소유주를 임의 지정받지 않고, 인증 컨텍스트의 사용자 식별자를 `owner_user_id`로 저장해야 한다. +- 시스템은 `admin` 권한을 제외한 모든 사용자/토큰 요청에서 `owner_user_id`가 인증 주체와 일치하는 포트폴리오만 조회/변경/삭제 가능해야 한다. +- 시스템은 `admin` 권한 요청에서 소유권 제약 없이 포트폴리오 전체를 조회/변경/삭제할 수 있어야 한다. +- 시스템은 소유권 불일치로 거부된 요청에 대해 `403 Forbidden`을 반환해야 한다. +- 시스템은 인증 컨텍스트에 사용자 식별자가 없는 토큰이 소유권이 필요한 포트폴리오 작업을 요청하면 `403 Forbidden`을 반환해야 한다. +- 시스템은 기존 포트폴리오 데이터에 대해 마이그레이션 시 `owner_user_id`를 null 허용으로 추가한 뒤, 백필 완료 후 not null 제약을 적용할 수 있어야 한다. + +## Acceptance Criteria +1. Given 인증된 일반 사용자(또는 해당 사용자에 귀속된 토큰)가 `POST /portfolios`를 호출할 때, When 포트폴리오가 생성되면, Then 저장된 `owner_user_id`는 요청자 사용자 식별자와 동일해야 하며 요청 본문으로 다른 소유주를 지정할 수 없어야 한다. +2. Given 서로 다른 두 사용자 A와 B가 존재하고 A 소유 포트폴리오가 있을 때, When B가 `GET /portfolios/{portfolio_id}` 또는 변경 API를 호출하면, Then 시스템은 `403`을 반환하고 데이터를 노출/변경하지 않아야 한다. +3. Given 일반 사용자 A가 목록 조회를 수행할 때, When `GET /portfolios`를 호출하면, Then 응답 목록에는 A 소유 포트폴리오만 포함되어야 한다. +4. Given `admin` 권한 사용자가 존재할 때, When `GET /portfolios` 또는 특정 포트폴리오 변경 API를 호출하면, Then 시스템은 소유권 검사로 요청을 거부하지 않아야 한다. +5. Given 마이그레이션 적용 전 생성된 레거시 포트폴리오가 존재할 때, When ownership 마이그레이션을 적용하면, Then 데이터 손실 없이 `owner_user_id`가 백필되고 이후 읽기/쓰기 API가 실패 없이 동작해야 한다. +6. Given 인증 컨텍스트에 사용자 식별자가 없는 bot 토큰이 포트폴리오 생성/수정/삭제를 요청할 때, When 소유권이 필요한 API를 호출하면, Then 시스템은 `403`을 반환해야 한다. + +## Impacted Area +- Docs: `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md`, `docs/technical/auth-and-token-lifecycle.md`, `docs/technical/api-contracts.md`, `docs/technical/persistence-and-migrations.md`, `docs/technical/portfolio-ownership-and-auth-design.md` +- Modules: `app/core/auth.py`, `app/api/portfolios.py`, `app/services/portfolio_service.py`, `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py`, `app/domain/models.py`, `app/schemas/portfolio.py`, `migrations/versions/*` +- Tests: `tests/test_app.py`, `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_domain_services.py` + +## Notes +- 본 requirement는 인증 서비스 분리 여부와 무관하게 portfolio-hub 내부에서 반드시 충족되어야 하는 리소스 소유권 계약이다. +- 소유권 기반 제어는 기존 RBAC(`read`, `trade-write`, `admin`)를 대체하지 않고 보완한다. + +## Change Log + +### CHG-20260413-01 +- Date: 2026-04-13 +- Author: GitHub Copilot + +#### Change Summary +- 포트폴리오 소유주 필드와 소유권 기반 접근 제어를 정의하는 신규 requirement를 추가했다. + +#### Intent +- frontend와 API가 동일 인증 체계를 사용할 때도 포트폴리오 단위 데이터 격리를 강제하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 소유주 저장 규칙 + - 일반 사용자 소유권 격리 규칙 + - admin 예외 규칙 + - 레거시 백필 규칙 + - user_id 없는 주체 거부 규칙 +- Updated: + - 없음 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` + - `docs/SRS/index.md` + - `docs/SRS/traceability-matrix.md` + - `docs/technical/portfolio-ownership-and-auth-design.md` +- Modules: + - ownership 검사 경로 전반 +- Tests: + - API/권한/백필 검증 시나리오 + +#### Validation Plan +- 사용자별 소유권 격리 통합 테스트, admin 우회 테스트, 마이그레이션 백필 테스트를 통해 계약 충족 여부를 확인한다. diff --git a/docs/SRS/index.md b/docs/SRS/index.md index c45327e..27cc8be 100644 --- a/docs/SRS/index.md +++ b/docs/SRS/index.md @@ -15,7 +15,7 @@ - 최근 변경 요약은 최신 항목을 위에 두고 최대 5건만 유지한다. ## Next Requirement ID -- `REQ-004` +- `REQ-005` ## Requirement Register @@ -24,13 +24,14 @@ | REQ-001 | 포트폴리오 댓글형 메모 이력 지원 | Interface | Approved | Medium | Justin | `docs/SRS/Interface/REQ-001_portfolio-comment-history.md` | | REQ-002 | 포트폴리오 태깅 및 태그 기반 검색 지원 | Interface | Proposed | Medium | Justin | `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` | | REQ-003 | 포트폴리오 필수 필드 및 거래소/시장 데이터 정의 | Data | Proposed | Medium | Justin | `docs/SRS/Data/REQ-003_portfolio-required-fields.md` | +| REQ-004 | 포트폴리오 소유주 기반 접근 제어 | Interface | Proposed | High | Justin | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | ## Recent Change Log Summary +- 2026-04-13 — `REQ-004` 포트폴리오 소유주(`owner_user_id`) 및 소유권 기반 접근 제어 requirement를 추가했다. - 2026-04-13 — `REQ-003`에서 태그 필터 기반 조회의 결정성 유지 책임을 소비 서비스(stock-picker)로 이관했다. - 2026-04-13 — `REQ-003`에서 `sourceVersion`/`sourceTimestamp` 요구를 제거하고 태그 필터 기반 조회 결정성 계약으로 정리했다. - 2026-04-13 — `REQ-003`의 소비자 표현을 특정 서비스 전용에서 태그 기반 조회 소비 서비스 일반 계약으로 정리했다. - 2026-04-13 — `REQ-002`의 소비자 표현을 특정 서비스 전용에서 다운스트림 소비자 일반 계약으로 정리했다. -- 2026-04-13 — `REQ-002`에 includeTags/excludeTags 태그 필터 계약을 추가했다. ## Status Transition Evidence - `Proposed` → `Approved`: Owner가 지정되고 requirement 문서 리뷰가 완료되며 acceptance criteria가 누락 없이 검토되어야 한다. diff --git a/docs/SRS/traceability-matrix.md b/docs/SRS/traceability-matrix.md index c5f0978..30ff263 100644 --- a/docs/SRS/traceability-matrix.md +++ b/docs/SRS/traceability-matrix.md @@ -14,6 +14,7 @@ | REQ-001 | AC1, AC2, AC3, AC4, AC5, AC6, AC7 | `docs/SRS/Interface/REQ-001_portfolio-comment-history.md` | #2 | TBD | TBD | Approved | In Progress | 2026-04-10 | | REQ-002 | AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9 | `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` | TBD | TBD | TBD | Proposed | Planned | 2026-04-10 | | REQ-003 | AC1, AC2, AC3, AC4, AC5 | `docs/SRS/Data/REQ-003_portfolio-required-fields.md` | TBD | TBD | TBD | Proposed | Planned | 2026-04-10 | +| REQ-004 | AC1, AC2, AC3, AC4, AC5, AC6 | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | TBD | TBD | `tests/test_portfolio_ownership.py`, `tests/test_auth.py`, `tests/test_portfolio_comments.py`, local `pytest` pass (2026-04-14) | Proposed | In Progress | 2026-04-14 | ## Update Rules - REQ ID must exactly match the SRS requirement file prefix. diff --git a/docs/technical/api-contracts.md b/docs/technical/api-contracts.md index 061173e..04dfabf 100644 --- a/docs/technical/api-contracts.md +++ b/docs/technical/api-contracts.md @@ -1,11 +1,12 @@ # API Contracts -- Related REQ: `REQ-001`, `REQ-002`, `REQ-003` +- Related REQ: `REQ-001`, `REQ-002`, `REQ-003`, `REQ-004` ## 공통 원칙 - 응답은 JSON 기반으로 반환한다. - 인증이 필요한 엔드포인트는 `Authorization: Bearer `을 사용한다. - 권한 계층은 `read` < `trade-write` < `admin`이다. +- `admin`이 아닌 요청은 포트폴리오 리소스 접근 시 `owner_user_id` 소유권 검사를 추가로 통과해야 한다. - 에러 응답은 HTTP 상태 코드와 함께 원인 메시지를 제공한다. - Base URL 예시는 `http://localhost:8000`을 기준으로 한다. - JSON 예시는 대표 형태이며 일부 선택 필드를 생략할 수 있다. @@ -95,6 +96,7 @@ #### `GET /portfolios` - Auth: `read` +- Ownership: `admin`은 전체 조회, 그 외는 본인 소유(`owner_user_id == auth_context.user_id`)만 조회 - Query: - `broker: string | null` - `status_filter: string | null` @@ -131,6 +133,7 @@ curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled #### `GET /portfolios/{portfolio_id}` - Auth: `read` +- Ownership: `admin`이 아니고 소유주 불일치 시 `403` - Query: - `recent_trades: int = 5` - 응답 `200`: `PortfolioDetailResponse` @@ -148,6 +151,7 @@ curl -X GET "http://localhost:8000/portfolios/krw-btc?recent_trades=5" \ "status": "enabled", "name": "Bitcoin", "symbol": "KRW-BTC", + "owner_user_id": "user-1", "cash_balance": 1000000.0, "asset_quantity": 0.1, "price": 100000000.0, @@ -188,6 +192,7 @@ curl -X GET "http://localhost:8000/portfolios/krw-btc?recent_trades=5" \ #### `POST /portfolios` - Auth: `trade-write` +- Ownership: 요청 body로 owner를 받지 않고 인증 컨텍스트 사용자로 `owner_user_id`를 강제 설정 - Body: `CreatePortfolioRequest` - 응답 `201`: `PortfolioDetailResponse` diff --git a/docs/technical/auth-and-token-lifecycle.md b/docs/technical/auth-and-token-lifecycle.md index 7893a28..1f96072 100644 --- a/docs/technical/auth-and-token-lifecycle.md +++ b/docs/technical/auth-and-token-lifecycle.md @@ -1,6 +1,6 @@ # Auth and Token Lifecycle -- Related REQ: N/A +- Related REQ: `REQ-004` ## 목표 정적 토큰과 DB 발급 토큰을 병행하는 현재 인증 모델의 동작 기준을 명확히 한다. @@ -13,6 +13,11 @@ | `trade-write` | 조회 + 포트폴리오 변경 API | | `admin` | 전체 API + 사용자/봇 관리 | +소유권 결합 정책: + +- `admin`이 아닌 주체는 포트폴리오 접근 시 리소스 소유주(`owner_user_id`) 일치 조건을 추가로 만족해야 한다. +- `admin`은 소유권 제약 없이 전체 포트폴리오를 접근할 수 있다. + ## 인증 방식 - 보호된 API는 `Authorization: Bearer ` 헤더를 사용한다. diff --git a/docs/technical/index.md b/docs/technical/index.md index 8a36f82..759cbac 100644 --- a/docs/technical/index.md +++ b/docs/technical/index.md @@ -21,6 +21,7 @@ | `api-contracts.md` | HTTP API 상세 계약, request/response 모델, 대표 payload 예시 | `REQ-001`, `REQ-002`, `REQ-003` | | `persistence-and-migrations.md` | PostgreSQL/Timescale 연동, 리포지토리 스위치, Alembic 운영, 스키마 baseline | `REQ-001`, `REQ-003` | | `auth-and-token-lifecycle.md` | 로그인/토큰/봇 주체 라이프사이클 및 권한 모델 | N/A | +| `portfolio-ownership-and-auth-design.md` | 포트폴리오 소유주(`owner_user_id`) 도입, 소유권 접근 제어, 검증 시나리오 설계 | `REQ-004` | | `management-cli-operations.md` | `portfoliohub` CLI 명령 책임, 종료코드, 운영 플로우 | N/A | | `deployment-and-operations.md` | Linux systemd, Caddy reverse proxy, Docker Compose 운영 절차 | N/A | @@ -31,6 +32,7 @@ | `REQ-001` | `api-contracts.md`, `persistence-and-migrations.md` | | `REQ-002` | `api-contracts.md` | | `REQ-003` | `architecture-overview.md`, `api-contracts.md`, `persistence-and-migrations.md` | +| `REQ-004` | `auth-and-token-lifecycle.md`, `api-contracts.md`, `persistence-and-migrations.md`, `portfolio-ownership-and-auth-design.md` | ## 권장 작성 순서 1. `project-structure-baseline.md` diff --git a/docs/technical/persistence-and-migrations.md b/docs/technical/persistence-and-migrations.md index 9eec26a..ffb0723 100644 --- a/docs/technical/persistence-and-migrations.md +++ b/docs/technical/persistence-and-migrations.md @@ -1,6 +1,6 @@ # Persistence and Migrations -- Related REQ: `REQ-001`, `REQ-003` +- Related REQ: `REQ-001`, `REQ-003`, `REQ-004` ## 목표 DB 기반 영속성 경로를 안정적으로 운영하고, 스키마 변경 이력과 문서 동기화를 일관되게 유지한다. @@ -28,7 +28,7 @@ DB 기반 영속성 경로를 안정적으로 운영하고, 스키마 변경 이 ## 스키마 문서 기준 - Source of truth: Alembic revisions in `migrations/versions` -- Current baseline revision: `20260410_0009` +- Current baseline revision: `20260413_0010` - 상세 테이블/컬럼/인덱스 제약은 이 문서가 Alembic revision과 동기화된 기준 문서다. ## 마이그레이션 운영 흐름 @@ -158,7 +158,7 @@ Purpose: Current baseline note: -- 아래 컬럼은 revision `20260403_0008`까지의 마이그레이션 기준이다. +- 아래 컬럼은 revision `20260413_0010`까지의 마이그레이션 기준이다. | Column | Type | Null | Default | Constraints / Notes | | --- | --- | --- | --- | --- | @@ -167,6 +167,7 @@ Current baseline note: | status | Text | NO | `enabled` | Portfolio status | | name | Text | NO | - | Asset name | | symbol | Text | NO | - | Trading symbol | +| owner_user_id | Text | NO | - | FK to `users.id` | | cash_balance | Float | NO | - | Current cash balance | | asset_quantity | Float | NO | - | Current asset quantity | | asset_price | Float | NO | 0 | Current price snapshot | @@ -180,7 +181,7 @@ Current baseline note: Constraints: - Primary key: `portfolio_id` -- Foreign key constraints: none in baseline +- Foreign key: `fk_portfolios_owner_user` on (`owner_user_id`) -> `users(id)` #### portfolio_ledger @@ -239,6 +240,7 @@ Constraints: | `idx_bot_principals_active_created` | `bot_principals` | `is_active`, `created_at` | no | active/inactive bot filtering | | `idx_portfolio_ledger_portfolio_time` | `portfolio_ledger` | `portfolio_id`, `recorded_at DESC` | no | recent ledger query | | `idx_portfolios_broker_status_updated` | `portfolios` | `broker`, `status`, `updated_at DESC` | no | broker/status recency filtering | +| `idx_portfolios_owner_updated` | `portfolios` | `owner_user_id`, `updated_at DESC` | no | owner-scoped recency filtering | | `idx_portfolio_comments_portfolio_created` | `portfolio_comments` | `portfolio_id`, `created_at DESC` | no | reverse chronological comments | Conditional migration behavior: @@ -248,9 +250,9 @@ Conditional migration behavior: ## Integrity Notes -현재 baseline은 DB-level FK constraint를 의도적으로 두지 않는다. +현재 baseline은 필수 ownership 제약(`portfolios.owner_user_id` -> `users.id`)만 DB-level FK를 사용한다. -- logical relation은 application/repository logic이 관리한다. +- ownership 외 logical relation은 application/repository logic이 관리한다. - 향후 FK constraint를 추가하면 migration ID와 함께 본 문서를 갱신한다. - application layer cascade: - 포트폴리오 삭제 시 연관 `portfolio_comments`를 hard delete 한다. @@ -273,6 +275,7 @@ Conditional migration behavior: | 20260403_0007 | migrations/versions/20260403_0007_bot_principal_permission_only.py | Remove `owner_user_id` and enforce bot-local permission model | | 20260403_0008 | migrations/versions/20260403_0008_portfolio_name_column.py | Add non-null `portfolios.name` and backfill from `symbol` | | 20260410_0009 | migrations/versions/20260410_0009_portfolio_comments_table.py | Add `portfolio_comments` table for per-portfolio memo history | +| 20260413_0010 | migrations/versions/20260413_0010_portfolio_owner_user_id.py | Add `portfolios.owner_user_id`, backfill owner, create owner index, and add owner FK | ## Open Implementation Points - `portfolio_ledger`의 Timescale hypertable 강제 여부와 보존 정책(retention)을 확정한다. diff --git a/docs/technical/portfolio-ownership-and-auth-design.md b/docs/technical/portfolio-ownership-and-auth-design.md new file mode 100644 index 0000000..9add3f2 --- /dev/null +++ b/docs/technical/portfolio-ownership-and-auth-design.md @@ -0,0 +1,137 @@ +# Portfolio Ownership and Auth Design + +- Related REQ: `REQ-004` + +## 목표 +- 포트폴리오 리소스에 소유주(`owner_user_id`)를 도입해 사용자 단위 데이터 격리를 강제한다. +- 기존 RBAC(`read`, `trade-write`, `admin`)와 소유권 검사 규칙을 결합해 접근 제어를 표준화한다. +- frontend와 portfolio-hub가 같은 서버에서 개별 컨테이너로 운영되는 구조에서도 단일 인증 체계로 일관된 권한 모델을 유지한다. + +## 범위 +- 포함: + - DB 스키마 변경 및 레거시 데이터 백필 + - API/서비스/리포지토리 ownership 정책 + - 통합 테스트 및 회귀 테스트 시나리오 +- 제외: + - 외부 IdP/OIDC 연동 + - 인증 서비스를 별도 프로세스로 분리하는 배포 작업 + +## DB 마이그레이션 설계 + +## Revision Plan +1. `owner_user_id` nullable 추가 +2. 레거시 row 백필 +3. 인덱스 추가 +4. not null 제약 적용 +5. 필요 시 FK 제약(`users.id`) 적용 + +## 권장 DDL 단계 +1. 컬럼 추가 + +```sql +ALTER TABLE portfolios +ADD COLUMN owner_user_id TEXT; +``` + +2. 백필 기준 사용자 결정 +- 환경변수 기반 bootstrap admin이 있으면 해당 사용자 id를 우선 사용한다. +- 없으면 `users.created_at ASC` 첫 사용자 id를 사용한다. +- 사용자 자체가 없으면 migration을 실패시키고 운영자가 사용자 생성 후 재시도한다. + +3. 레거시 데이터 백필 + +```sql +UPDATE portfolios +SET owner_user_id = :fallback_user_id +WHERE owner_user_id IS NULL; +``` + +4. 제약 및 인덱스 적용 + +```sql +ALTER TABLE portfolios +ALTER COLUMN owner_user_id SET NOT NULL; + +CREATE INDEX idx_portfolios_owner_updated +ON portfolios (owner_user_id, updated_at DESC); +``` + +5. 선택적 FK 제약 + +```sql +ALTER TABLE portfolios +ADD CONSTRAINT fk_portfolios_owner_user +FOREIGN KEY (owner_user_id) REFERENCES users(id); +``` + +## 롤백 전략 +- downgrade에서는 FK, 인덱스, not null 제약 해제 후 컬럼 제거 순서로 진행한다. +- 운영 데이터 보존이 필요하면 컬럼 삭제 대신 nullable 유지 downgrade를 별도 운영 방침으로 둔다. + +## API 권한 규칙 설계 + +## 공통 정책 +- 1단계: RBAC 검사 통과 필요 +- 2단계: admin이 아니면 ownership 검사 수행 +- ownership 검사 기준: `portfolio.owner_user_id == auth_context.user_id` +- `auth_context.user_id`가 없는 요청은 ownership 필요한 엔드포인트에서 `403` + +## Endpoint Policy Matrix + +| Endpoint | RBAC 최소 권한 | Ownership 적용 | 비고 | +| --- | --- | --- | --- | +| `GET /portfolios` | read | 적용 | admin은 전체, 그 외 owner만 필터 | +| `GET /portfolios/{portfolio_id}` | read | 적용 | owner 불일치 시 403 | +| `POST /portfolios` | trade-write | 생성 시 owner 고정 | owner_user_id는 인증 주체로 강제 | +| `PATCH /portfolios/{portfolio_id}` | trade-write | 적용 | owner 불일치 시 403 | +| `POST /portfolios/{portfolio_id}/buy` | trade-write | 적용 | owner 불일치 시 403 | +| `POST /portfolios/{portfolio_id}/sell` | trade-write | 적용 | owner 불일치 시 403 | +| `POST /portfolios/{portfolio_id}/invest` | trade-write | 적용 | owner 불일치 시 403 | +| `POST /portfolios/{portfolio_id}/transfer` | trade-write | 적용 | owner 불일치 시 403 | +| `POST /portfolios/{portfolio_id}/reset` | trade-write | 적용 | owner 불일치 시 403 | +| `DELETE /portfolios/{portfolio_id}` | admin | admin only | 기존 정책 유지 | +| `POST/GET/PATCH/DELETE /portfolios/{id}/comments*` | read or trade-write | 적용 | 상위 포트폴리오 ownership을 재사용 | + +## 서비스/리포지토리 반영 포인트 +- Domain model: + - `Portfolio`에 `owner_user_id` 추가 +- Schema: + - `PortfolioSummary`/`CreatePortfolioRequest` 응답/입력 계약에 owner 노출 정책 정의 + - 외부 입력으로 owner_user_id 수동 지정은 금지 +- Repository: + - create 시 owner_user_id 필수 인자 + - list/get 시 요청자 컨텍스트 기반 owner 필터 함수 제공 +- Service/API: + - request principal을 service 계층으로 전달 + - 공통 ownership 가드 함수 도입 + +## frontend 연계 방침 +- frontend는 credential 저장/검증 자체를 수행하지 않고 portfolio-hub 인증 API를 사용한다. +- 브라우저 경로는 HttpOnly + Secure cookie 기반 세션을 우선 고려한다. +- 동일 서버 내 별도 컨테이너 운영 시 reverse proxy에서 CORS/Cookie domain 정책을 일관되게 관리한다. + +## 테스트 케이스 목록 + +## Migration +1. 기존 portfolios row가 존재할 때 owner_user_id 백필 후 null row가 0건인지 검증 +2. users가 없는 상태에서 migration 실행 시 실패 동작 검증 +3. downgrade 시 인덱스/제약 해제가 순서대로 수행되는지 검증 + +## API Authorization +1. 사용자 A 소유 portfolio를 사용자 B가 조회하면 403 +2. 사용자 A 소유 portfolio를 사용자 B가 수정/매매/리셋하면 403 +3. admin이 타 사용자 portfolio 조회/수정 가능 +4. 일반 사용자의 `GET /portfolios`는 본인 소유만 반환 +5. user_id 없는 bot principal 요청이 ownership 필요 API 호출 시 403 + +## API Creation +1. `POST /portfolios`에서 owner_user_id가 인증 사용자로 저장되는지 검증 +2. 요청 body에 owner_user_id를 넣어도 무시되거나 validation 실패하는지 검증 + +## Regression +1. 기존 REQ-001 댓글 API가 ownership 정책 적용 후에도 owner 경계 내에서 정상 동작 +2. 기존 RBAC 오류(`401`, `403`) 계약이 ownership 도입 후에도 유지 +3. admin delete 동작이 ownership 변경과 충돌 없이 유지 + +## 추적 항목 +- 본 설계 문서 구현 시 `docs/SRS/traceability-matrix.md`의 `REQ-004` 행에 Issue/PR/Test evidence를 갱신한다. diff --git a/migrations/versions/20260413_0010_portfolio_owner_user_id.py b/migrations/versions/20260413_0010_portfolio_owner_user_id.py new file mode 100644 index 0000000..47f2550 --- /dev/null +++ b/migrations/versions/20260413_0010_portfolio_owner_user_id.py @@ -0,0 +1,97 @@ +"""add portfolio owner user id + +Revision ID: 20260413_0010 +Revises: 20260410_0009 +Create Date: 2026-04-13 01:00:00 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +from sqlalchemy import inspect + + +revision = "20260413_0010" +down_revision = "20260410_0009" +branch_labels = None +depends_on = None + + +def _column_exists(inspector, table_name: str, column_name: str) -> bool: + return any(column["name"] == column_name for column in inspector.get_columns(table_name)) + + +def _index_exists(inspector, table_name: str, index_name: str) -> bool: + return any(index["name"] == index_name for index in inspector.get_indexes(table_name)) + + +def _fk_exists(inspector, table_name: str, fk_name: str) -> bool: + return any(fk.get("name") == fk_name for fk in inspector.get_foreign_keys(table_name)) + + +def upgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + + if not _column_exists(inspector, "portfolios", "owner_user_id"): + op.add_column("portfolios", sa.Column("owner_user_id", sa.Text(), nullable=True)) + + bind.execute( + sa.text( + """ + WITH fallback_owner AS ( + SELECT id + FROM users + ORDER BY created_at ASC, id ASC + LIMIT 1 + ) + UPDATE portfolios p + SET owner_user_id = f.id + FROM fallback_owner f + WHERE p.owner_user_id IS NULL + """ + ) + ) + + unresolved = bind.execute(sa.text("SELECT COUNT(*) FROM portfolios WHERE owner_user_id IS NULL")).scalar() + if unresolved and int(unresolved) > 0: + raise RuntimeError("owner_user_id backfill failed: users 테이블에 fallback 소유주가 필요합니다") + + op.alter_column("portfolios", "owner_user_id", existing_type=sa.Text(), nullable=False) + + inspector = inspect(bind) + if not _index_exists(inspector, "portfolios", "idx_portfolios_owner_updated"): + op.create_index( + "idx_portfolios_owner_updated", + "portfolios", + ["owner_user_id", sa.text("updated_at DESC")], + unique=False, + ) + + inspector = inspect(bind) + if not _fk_exists(inspector, "portfolios", "fk_portfolios_owner_user"): + op.create_foreign_key( + "fk_portfolios_owner_user", + "portfolios", + "users", + ["owner_user_id"], + ["id"], + ondelete="RESTRICT", + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + + if _fk_exists(inspector, "portfolios", "fk_portfolios_owner_user"): + op.drop_constraint("fk_portfolios_owner_user", "portfolios", type_="foreignkey") + + inspector = inspect(bind) + if _index_exists(inspector, "portfolios", "idx_portfolios_owner_updated"): + op.drop_index("idx_portfolios_owner_updated", table_name="portfolios") + + inspector = inspect(bind) + if _column_exists(inspector, "portfolios", "owner_user_id"): + op.drop_column("portfolios", "owner_user_id") diff --git a/tests/test_cli.py b/tests/test_cli.py index 0f5cf38..568f40c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -117,6 +117,7 @@ class FakeRepo: status: str = "enabled", active_strategy_id: str | None = None, avabile_strategies: list[str] | None = None, + owner_user_id: str | None = None, initial_cash: float = 0.0, ) -> Portfolio: if self._portfolio is None: @@ -126,6 +127,7 @@ class FakeRepo: status=status, name=name or symbol, symbol=symbol, + owner_user_id=owner_user_id, cash_balance=initial_cash, active_strategy_id=active_strategy_id, avabile_strategies=avabile_strategies, @@ -136,6 +138,7 @@ class FakeRepo: self, portfolio_id: str, *, + owner_user_id: str, broker: str, status: str, name: str, @@ -153,6 +156,7 @@ class FakeRepo: status=status, name=name, symbol=symbol, + owner_user_id=owner_user_id, cash_balance=cash_balance, asset_quantity=asset_quantity, price=asset_price, @@ -169,13 +173,21 @@ class FakeRepo: setattr(portfolio, key, value) return portfolio - def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: + def list_portfolios( + self, + *, + broker: str | None = None, + status: str | None = None, + owner_user_id: str | None = None, + ) -> list[Portfolio]: if self._portfolio is None: return [] if broker is not None and self._portfolio.broker != broker: return [] if status is not None and self._portfolio.status != status: return [] + if owner_user_id is not None and self._portfolio.owner_user_id != owner_user_id: + return [] return [self._portfolio] def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: diff --git a/tests/test_domain_services.py b/tests/test_domain_services.py index ee9e31f..480869f 100644 --- a/tests/test_domain_services.py +++ b/tests/test_domain_services.py @@ -31,6 +31,7 @@ class FakeRepo: status: str = "enabled", active_strategy_id: str | None = None, avabile_strategies: list[str] | None = None, + owner_user_id: str | None = None, initial_cash: float = 0.0, ) -> Portfolio: self._portfolio = Portfolio( @@ -39,6 +40,7 @@ class FakeRepo: status=status, name=name or symbol, symbol=symbol, + owner_user_id=owner_user_id, cash_balance=initial_cash, active_strategy_id=active_strategy_id, avabile_strategies=avabile_strategies, @@ -55,9 +57,17 @@ class FakeRepo: setattr(portfolio, key, value) return portfolio - def list_portfolios(self, *, broker: str | None = None, status: str | None = None) -> list[Portfolio]: + def list_portfolios( + self, + *, + broker: str | None = None, + status: str | None = None, + owner_user_id: str | None = None, + ) -> list[Portfolio]: if self._portfolio is None: return [] + if owner_user_id is not None and self._portfolio.owner_user_id != owner_user_id: + return [] return [self._portfolio] def get_strategy_set(self, id: str) -> StrategySet | None: diff --git a/tests/test_portfolio_comments.py b/tests/test_portfolio_comments.py index d9d30d2..8e56989 100644 --- a/tests/test_portfolio_comments.py +++ b/tests/test_portfolio_comments.py @@ -50,7 +50,9 @@ class FakeRepo: setattr(portfolio, key, value) return portfolio - def list_portfolios(self, *, broker=None, status=None): + def list_portfolios(self, *, broker=None, status=None, owner_user_id=None): + if owner_user_id is not None and self._portfolio and self._portfolio.owner_user_id != owner_user_id: + return [] return [self._portfolio] if self._portfolio else [] def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: @@ -181,6 +183,7 @@ def _portfolio() -> Portfolio: status="enabled", name="Samsung Electronics", symbol="005930", + owner_user_id="legacy-owner", cash_balance=1_000_000.0, asset_quantity=1.0, price=70_000.0, diff --git a/tests/test_portfolio_ownership.py b/tests/test_portfolio_ownership.py new file mode 100644 index 0000000..a66fe90 --- /dev/null +++ b/tests/test_portfolio_ownership.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +from fastapi.testclient import TestClient + +import app.api.portfolios as portfolios_api +import app.core.auth as auth_core +from app.core.config import get_settings +from app.core.security import hash_token +from app.domain.models import LedgerEntry, Portfolio, StrategySet, TokenPrincipal +from app.main import app + + +class FakeRepo: + def __init__(self, *, portfolio: Portfolio, principals: dict[str, TokenPrincipal]) -> None: + self._portfolio = portfolio + self._principals = principals + self._strategy = StrategySet(id="set-1", name="target_value", config="{}", nick="target_value:demo") + + def close(self) -> None: + return None + + def list_users(self): + return [] + + def get_token_principal(self, token_hash: str): + return self._principals.get(token_hash) + + def get(self, portfolio_id: str) -> Portfolio: + if self._portfolio.portfolio_id != portfolio_id: + raise ValueError("포트폴리오를 찾을 수 없습니다") + return self._portfolio + + def list_recent_trades(self, portfolio_id: str, limit: int = 5) -> list[LedgerEntry]: + return [] + + def get_strategy_set(self, id: str): + if id == self._strategy.id: + return self._strategy + return None + + +def _portfolio(owner_user_id: str) -> Portfolio: + return Portfolio( + portfolio_id="p1", + broker="kis", + status="enabled", + name="Samsung Electronics", + symbol="005930", + owner_user_id=owner_user_id, + cash_balance=1_000_000.0, + asset_quantity=1.0, + price=70_000.0, + profit_buffer=0.0, + active_strategy_id="set-1", + avabile_strategies=["set-1"], + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + +def _configure_client(monkeypatch, repo: FakeRepo) -> TestClient: + monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "true") + monkeypatch.delenv("PORTFOLIOHUB_READ_TOKENS", raising=False) + monkeypatch.delenv("PORTFOLIOHUB_TRADE_WRITE_TOKENS", raising=False) + monkeypatch.delenv("PORTFOLIOHUB_ADMIN_TOKENS", raising=False) + get_settings.cache_clear() + app.dependency_overrides.clear() + monkeypatch.setattr(portfolios_api, "get_portfolio_repository_from_settings", lambda settings: repo) + monkeypatch.setattr(auth_core, "get_portfolio_repository_from_settings", lambda settings: repo) + return TestClient(app) + + +def test_owner_can_read_owned_portfolio_on_db_auth(monkeypatch) -> None: + token = "owner-db-token" + principal = TokenPrincipal( + token_id="t1", + user_id="owner-1", + permission="read", + token_type="client", + ) + repo = FakeRepo(portfolio=_portfolio("owner-1"), principals={hash_token(token): principal}) + client = _configure_client(monkeypatch, repo) + + response = client.get("/portfolios/p1", headers={"Authorization": f"Bearer {token}"}) + + app.dependency_overrides.clear() + assert response.status_code == 200 + assert response.json()["portfolio"]["owner_user_id"] == "owner-1" + + +def test_non_owner_gets_403_on_db_auth(monkeypatch) -> None: + token = "other-db-token" + principal = TokenPrincipal( + token_id="t2", + user_id="other-user", + permission="read", + token_type="client", + ) + repo = FakeRepo(portfolio=_portfolio("owner-1"), principals={hash_token(token): principal}) + client = _configure_client(monkeypatch, repo) + + response = client.get("/portfolios/p1", headers={"Authorization": f"Bearer {token}"}) + + app.dependency_overrides.clear() + assert response.status_code == 403 + assert response.json()["detail"] == "Portfolio ownership mismatch" + + +def test_db_enabled_env_token_without_user_context_is_rejected(monkeypatch) -> None: + repo = FakeRepo(portfolio=_portfolio("owner-1"), principals={}) + monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "true") + monkeypatch.setenv("PORTFOLIOHUB_TRADE_WRITE_TOKENS", "trade-env-token") + monkeypatch.delenv("PORTFOLIOHUB_READ_TOKENS", raising=False) + monkeypatch.delenv("PORTFOLIOHUB_ADMIN_TOKENS", raising=False) + get_settings.cache_clear() + app.dependency_overrides.clear() + monkeypatch.setattr(portfolios_api, "get_portfolio_repository_from_settings", lambda settings: repo) + monkeypatch.setattr(auth_core, "get_portfolio_repository_from_settings", lambda settings: repo) + client = TestClient(app) + + response = client.post( + "/portfolios", + headers={"Authorization": "Bearer trade-env-token"}, + json={"portfolio_id": "new-1", "symbol": "005930"}, + ) + + app.dependency_overrides.clear() + assert response.status_code == 403 + assert response.json()["detail"] == "Ownership context is required" From 2dab17cbb15337402af513c72555f305932ae230 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 00:34:44 +0900 Subject: [PATCH 06/21] feat(auth): owner-bound bot tokens and self-service bot management --- app/api/auth_routes.py | 79 ++++++++++++++- app/domain/models.py | 1 + app/repositories/portfolio_repository.py | 8 +- app/repositories/postgres.py | 90 ++++++++++++++--- app/schemas/auth.py | 30 +++++- app/services/auth_service.py | 56 ++++++++++- ...-004_portfolio-ownership-access-control.md | 45 ++++++++- docs/SRS/index.md | 2 +- docs/SRS/traceability-matrix.md | 2 +- docs/technical/api-contracts.md | 50 +++++++++- docs/technical/auth-and-token-lifecycle.md | 25 +++++ .../portfolio-ownership-and-auth-design.md | 16 ++- ...260414_0011_bot_principal_owner_user_id.py | 97 +++++++++++++++++++ tests/test_portfolio_comments.py | 2 +- tests/test_portfolio_ownership.py | 19 ++++ tests/test_users.py | 89 ++++++++++++++++- 16 files changed, 576 insertions(+), 35 deletions(-) create mode 100644 migrations/versions/20260414_0011_bot_principal_owner_user_id.py diff --git a/app/api/auth_routes.py b/app/api/auth_routes.py index 57b4756..60132eb 100644 --- a/app/api/auth_routes.py +++ b/app/api/auth_routes.py @@ -14,6 +14,7 @@ from app.schemas.auth import ( CreateBotPrincipalRequest, CreateBotTokenRequest, CreateClientTokenRequest, + CreateSelfBotPrincipalRequest, IssuedBotTokenResponse, IssuedClientTokenResponse, LoginRequest, @@ -58,6 +59,7 @@ def _to_bot_principal_response(bot: BotPrincipal) -> BotPrincipalResponse: return BotPrincipalResponse( id=bot.id, name=bot.name, + owner_user_id=bot.owner_user_id, permission=bot.permission, description=bot.description, is_active=bot.is_active, @@ -94,7 +96,7 @@ def _raise_auth_error(exc: ValueError) -> NoReturn: detail = str(exc) if "잘못된 사용자명 또는 비밀번호" in detail: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=detail) from exc - if "초과" in detail: + if "초과" in detail or "권한이 없습니다" in detail: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail) from exc if "찾을 수 없습니다" in detail: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail) from exc @@ -156,6 +158,81 @@ def create_bot_principal( _raise_auth_error(exc) +@router.post("/me/bots", response_model=BotPrincipalResponse, status_code=status.HTTP_201_CREATED, dependencies=[Depends(require_read)]) +def create_owned_bot_principal( + payload: CreateSelfBotPrincipalRequest, + auth_context: AuthContext = Depends(require_read), + service: AuthService = Depends(get_auth_service), +) -> BotPrincipalResponse: + try: + return _to_bot_principal_response(service.create_owned_bot_principal(auth_context, payload)) + except ValueError as exc: + _raise_auth_error(exc) + + +@router.get("/me/bots", response_model=list[BotPrincipalResponse], dependencies=[Depends(require_read)]) +def list_owned_bot_principals( + auth_context: AuthContext = Depends(require_read), + is_active: bool | None = None, + service: AuthService = Depends(get_auth_service), +) -> list[BotPrincipalResponse]: + try: + return [ + _to_bot_principal_response(bot) + for bot in service.list_owned_bot_principals(auth_context, is_active=is_active) + ] + except ValueError as exc: + _raise_auth_error(exc) + + +@router.post( + "/me/bots/{bot_id}/tokens", + response_model=IssuedBotTokenResponse, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(require_read)], +) +def issue_owned_bot_token( + bot_id: str, + payload: CreateBotTokenRequest, + auth_context: AuthContext = Depends(require_read), + service: AuthService = Depends(get_auth_service), +) -> IssuedBotTokenResponse: + try: + return _to_issued_bot_token_response(service.issue_owned_bot_token(auth_context, bot_id, payload)) + except ValueError as exc: + _raise_auth_error(exc) + + +@router.get("/me/bots/{bot_id}/tokens", response_model=list[AuthTokenResponse], dependencies=[Depends(require_read)]) +def list_owned_bot_tokens( + bot_id: str, + auth_context: AuthContext = Depends(require_read), + service: AuthService = Depends(get_auth_service), +) -> list[AuthTokenResponse]: + try: + return [_to_auth_token_response(token) for token in service.list_owned_bot_tokens(auth_context, bot_id)] + except ValueError as exc: + _raise_auth_error(exc) + + +@router.delete( + "/me/bots/{bot_id}/tokens/{token_id}", + status_code=status.HTTP_204_NO_CONTENT, + dependencies=[Depends(require_read)], +) +def revoke_owned_bot_token( + bot_id: str, + token_id: str, + auth_context: AuthContext = Depends(require_read), + service: AuthService = Depends(get_auth_service), +) -> Response: + try: + service.revoke_owned_bot_token(auth_context, bot_id, token_id) + except ValueError as exc: + _raise_auth_error(exc) + return Response(status_code=status.HTTP_204_NO_CONTENT) + + @router.get("/bots", response_model=list[BotPrincipalResponse], dependencies=[Depends(require_admin)]) def list_bot_principals( is_active: bool | None = None, diff --git a/app/domain/models.py b/app/domain/models.py index d78e91a..a0030ae 100644 --- a/app/domain/models.py +++ b/app/domain/models.py @@ -57,6 +57,7 @@ class TokenPrincipal: class BotPrincipal: id: str name: str + owner_user_id: str permission: str description: str | None = None is_active: bool = True diff --git a/app/repositories/portfolio_repository.py b/app/repositories/portfolio_repository.py index 187fea4..18962a0 100644 --- a/app/repositories/portfolio_repository.py +++ b/app/repositories/portfolio_repository.py @@ -63,13 +63,19 @@ class PortfolioRepository(Protocol): self, *, name: str, + owner_user_id: str, permission: str, description: str | None = None, ) -> BotPrincipal: ... def get_bot_principal(self, bot_principal_id: str) -> BotPrincipal: ... - def list_bot_principals(self, *, is_active: bool | None = None) -> list[BotPrincipal]: ... + def list_bot_principals( + self, + *, + is_active: bool | None = None, + owner_user_id: str | None = None, + ) -> list[BotPrincipal]: ... def deactivate_bot_principal(self, bot_principal_id: str) -> BotPrincipal: ... diff --git a/app/repositories/postgres.py b/app/repositories/postgres.py index 4a54005..a0a10b6 100644 --- a/app/repositories/postgres.py +++ b/app/repositories/postgres.py @@ -48,6 +48,7 @@ _SELECT_BOT_PRINCIPAL = """ SELECT id, name, + owner_user_id, permission, description, is_active, @@ -161,6 +162,7 @@ class PostgresPortfolioRepository: CREATE TABLE IF NOT EXISTS bot_principals ( id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, + owner_user_id TEXT, permission TEXT NOT NULL, description TEXT, is_active BOOLEAN NOT NULL DEFAULT TRUE, @@ -171,9 +173,47 @@ class PostgresPortfolioRepository: """ ) cur.execute("ALTER TABLE auth_tokens ADD COLUMN IF NOT EXISTS bot_principal_id TEXT") + cur.execute("ALTER TABLE bot_principals ADD COLUMN IF NOT EXISTS owner_user_id TEXT") cur.execute("ALTER TABLE bot_principals ADD COLUMN IF NOT EXISTS permission TEXT") cur.execute("UPDATE bot_principals SET permission = 'read' WHERE permission IS NULL") cur.execute("ALTER TABLE bot_principals ALTER COLUMN permission SET NOT NULL") + cur.execute( + """ + UPDATE bot_principals + SET owner_user_id = fallback.id + FROM ( + SELECT id + FROM users + ORDER BY created_at ASC, id ASC + LIMIT 1 + ) AS fallback + WHERE owner_user_id IS NULL + """ + ) + cur.execute("SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL") + unresolved_bot_owners = int(cur.fetchone()[0]) + if unresolved_bot_owners > 0: + raise RuntimeError("bot_principals.owner_user_id backfill failed: users 테이블에 소유주가 필요합니다") + cur.execute("ALTER TABLE bot_principals ALTER COLUMN owner_user_id SET NOT NULL") + cur.execute( + """ + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'fk_bot_principals_owner_user' + ) THEN + ALTER TABLE bot_principals + ADD CONSTRAINT fk_bot_principals_owner_user + FOREIGN KEY (owner_user_id) + REFERENCES users(id) + ON DELETE RESTRICT; + END IF; + END + $$ + """ + ) cur.execute( """ CREATE INDEX IF NOT EXISTS idx_auth_tokens_user_created @@ -198,6 +238,12 @@ class PostgresPortfolioRepository: ON bot_principals (is_active, created_at DESC) """ ) + cur.execute( + """ + CREATE INDEX IF NOT EXISTS idx_bot_principals_owner_active_created + ON bot_principals (owner_user_id, is_active, created_at DESC) + """ + ) cur.execute( """ CREATE TABLE IF NOT EXISTS strategy_sets ( @@ -333,12 +379,13 @@ class PostgresPortfolioRepository: return BotPrincipal( id=row[0], name=row[1], - permission=row[2], - description=row[3] if len(row) > 3 else None, - is_active=bool(row[4]) if len(row) > 4 else True, - created_at=row[5] if len(row) > 5 else None, - updated_at=row[6] if len(row) > 6 else None, - deactivated_at=row[7] if len(row) > 7 else None, + owner_user_id=row[2], + permission=row[3], + description=row[4] if len(row) > 4 else None, + is_active=bool(row[5]) if len(row) > 5 else True, + created_at=row[6] if len(row) > 6 else None, + updated_at=row[7] if len(row) > 7 else None, + deactivated_at=row[8] if len(row) > 8 else None, ) @staticmethod @@ -594,6 +641,7 @@ class PostgresPortfolioRepository: self, *, name: str, + owner_user_id: str, permission: str, description: str | None = None, ) -> BotPrincipal: @@ -602,15 +650,18 @@ class PostgresPortfolioRepository: cur.execute("SELECT 1 FROM bot_principals WHERE name = %s", (name,)) if cur.fetchone() is not None: raise ValueError(f"이미 존재하는 bot name입니다: {name}") + cur.execute("SELECT 1 FROM users WHERE id = %s", (owner_user_id,)) + if cur.fetchone() is None: + raise ValueError("사용자를 찾을 수 없습니다") bot_id = str(uuid.uuid4()) cur.execute( """ - INSERT INTO bot_principals (id, name, permission, description) - VALUES (%s, %s, %s, %s) - RETURNING id, name, permission, description, is_active, created_at, updated_at, deactivated_at + INSERT INTO bot_principals (id, name, owner_user_id, permission, description) + VALUES (%s, %s, %s, %s, %s) + RETURNING id, name, owner_user_id, permission, description, is_active, created_at, updated_at, deactivated_at """, - (bot_id, name, permission, description), + (bot_id, name, owner_user_id, permission, description), ) row = cur.fetchone() return self._row_to_bot_principal(row) @@ -622,13 +673,24 @@ class PostgresPortfolioRepository: row = cur.fetchone() return self._row_to_bot_principal(row) - def list_bot_principals(self, *, is_active: bool | None = None) -> list[BotPrincipal]: + def list_bot_principals( + self, + *, + is_active: bool | None = None, + owner_user_id: str | None = None, + ) -> list[BotPrincipal]: self.ensure_schema() query = _SELECT_BOT_PRINCIPAL params: list[object] = [] + clauses: list[str] = [] if is_active is not None: - query += "WHERE is_active = %s " + clauses.append("is_active = %s") params.append(is_active) + if owner_user_id is not None: + clauses.append("owner_user_id = %s") + params.append(owner_user_id) + if clauses: + query += "WHERE " + " AND ".join(clauses) + " " query += "ORDER BY created_at DESC, name ASC" with self._connect().cursor() as cur: cur.execute(query, tuple(params)) @@ -643,7 +705,7 @@ class PostgresPortfolioRepository: UPDATE bot_principals SET is_active = FALSE, deactivated_at = NOW(), updated_at = NOW() WHERE id = %s - RETURNING id, name, permission, description, is_active, created_at, updated_at, deactivated_at + RETURNING id, name, owner_user_id, permission, description, is_active, created_at, updated_at, deactivated_at """, (bot_principal_id,), ) @@ -691,7 +753,7 @@ class PostgresPortfolioRepository: with self._connect().cursor() as cur: cur.execute( """ - SELECT t.id, t.user_id, t.permission, t.token_type, t.bot_principal_id + SELECT t.id, COALESCE(b.owner_user_id, t.user_id), t.permission, t.token_type, t.bot_principal_id FROM auth_tokens t LEFT JOIN bot_principals b ON b.id = t.bot_principal_id WHERE token_hash = %s diff --git a/app/schemas/auth.py b/app/schemas/auth.py index 3257aa9..db11afc 100644 --- a/app/schemas/auth.py +++ b/app/schemas/auth.py @@ -67,7 +67,30 @@ class IssuedClientTokenResponse(BaseModel): class CreateBotPrincipalRequest(BaseModel): name: str = Field(min_length=1) - permission: Literal["read", "trade-write", "admin"] + owner_user_id: str = Field(min_length=1) + permission: Literal["read", "trade-write"] + description: str | None = None + + @field_validator("name") + @classmethod + def normalize_bot_name(cls, value: str) -> str: + normalized = value.strip() + if not normalized: + raise ValueError("name은 비어 있을 수 없습니다") + return normalized + + @field_validator("owner_user_id") + @classmethod + def normalize_owner_user_id(cls, value: str) -> str: + normalized = value.strip() + if not normalized: + raise ValueError("owner_user_id는 비어 있을 수 없습니다") + return normalized + + +class CreateSelfBotPrincipalRequest(BaseModel): + name: str = Field(min_length=1) + permission: Literal["read", "trade-write"] description: str | None = None @field_validator("name") @@ -82,7 +105,8 @@ class CreateBotPrincipalRequest(BaseModel): class BotPrincipalResponse(BaseModel): id: str name: str - permission: Literal["read", "trade-write", "admin"] + owner_user_id: str + permission: Literal["read", "trade-write"] description: str | None = None is_active: bool created_at: datetime | None = None @@ -92,7 +116,7 @@ class BotPrincipalResponse(BaseModel): class CreateBotTokenRequest(BaseModel): name: str = Field(min_length=1) - permission: Literal["read", "trade-write", "admin"] | None = None + permission: Literal["read", "trade-write"] | None = None expires_in_days: int | None = Field(default=None, ge=1, le=3650) @field_validator("name") diff --git a/app/services/auth_service.py b/app/services/auth_service.py index e238d9f..9f7ec55 100644 --- a/app/services/auth_service.py +++ b/app/services/auth_service.py @@ -13,7 +13,13 @@ from app.core.security import generate_token, hash_token, token_prefix, verify_p from app.domain.models import AuthToken, BotPrincipal, User from app.repositories.factory import get_portfolio_repository_from_settings from app.repositories.portfolio_repository import PortfolioRepository -from app.schemas.auth import CreateBotPrincipalRequest, CreateBotTokenRequest, CreateClientTokenRequest, LoginRequest +from app.schemas.auth import ( + CreateBotPrincipalRequest, + CreateBotTokenRequest, + CreateClientTokenRequest, + CreateSelfBotPrincipalRequest, + LoginRequest, +) @dataclass @@ -105,6 +111,16 @@ class AuthService: def create_bot_principal(self, payload: CreateBotPrincipalRequest) -> BotPrincipal: return self._repository.create_bot_principal( name=payload.name, + owner_user_id=payload.owner_user_id, + permission=payload.permission, + description=payload.description, + ) + + def create_owned_bot_principal(self, auth_context: AuthContext, payload: CreateSelfBotPrincipalRequest) -> BotPrincipal: + user_id = self._require_db_user_context(auth_context) + return self._repository.create_bot_principal( + name=payload.name, + owner_user_id=user_id, permission=payload.permission, description=payload.description, ) @@ -112,6 +128,10 @@ class AuthService: def list_bot_principals(self, *, is_active: bool | None = None) -> list[BotPrincipal]: return self._repository.list_bot_principals(is_active=is_active) + def list_owned_bot_principals(self, auth_context: AuthContext, *, is_active: bool | None = None) -> list[BotPrincipal]: + user_id = self._require_db_user_context(auth_context) + return self._repository.list_bot_principals(is_active=is_active, owner_user_id=user_id) + def get_bot_principal(self, bot_principal_id: str) -> BotPrincipal: return self._repository.get_bot_principal(bot_principal_id) @@ -142,14 +162,48 @@ class AuthService: ) return IssuedBotToken(token=raw_token, auth_token=auth_token) + def issue_owned_bot_token( + self, + auth_context: AuthContext, + bot_principal_id: str, + payload: CreateBotTokenRequest, + ) -> IssuedBotToken: + user_id = self._require_db_user_context(auth_context) + bot = self._repository.get_bot_principal(bot_principal_id) + if bot.owner_user_id != user_id: + raise ValueError("봇 접근 권한이 없습니다") + return self.issue_bot_token(bot_principal_id, payload) + def list_bot_tokens(self, bot_principal_id: str) -> list[AuthToken]: self._repository.get_bot_principal(bot_principal_id) return self._repository.list_bot_auth_tokens(bot_principal_id) + def list_owned_bot_tokens(self, auth_context: AuthContext, bot_principal_id: str) -> list[AuthToken]: + user_id = self._require_db_user_context(auth_context) + bot = self._repository.get_bot_principal(bot_principal_id) + if bot.owner_user_id != user_id: + raise ValueError("봇 접근 권한이 없습니다") + return self._repository.list_bot_auth_tokens(bot_principal_id) + def revoke_bot_token(self, bot_principal_id: str, token_id: str) -> None: self._repository.get_bot_principal(bot_principal_id) self._repository.revoke_bot_auth_token(bot_principal_id, token_id) + def revoke_owned_bot_token(self, auth_context: AuthContext, bot_principal_id: str, token_id: str) -> None: + user_id = self._require_db_user_context(auth_context) + bot = self._repository.get_bot_principal(bot_principal_id) + if bot.owner_user_id != user_id: + raise ValueError("봇 접근 권한이 없습니다") + self._repository.revoke_bot_auth_token(bot_principal_id, token_id) + + @staticmethod + def _require_db_user_context(auth_context: AuthContext) -> str: + if auth_context.bot_principal_id is not None: + raise ValueError("봇 토큰으로는 사용자 전용 봇 관리를 수행할 수 없습니다") + if auth_context.user_id is None: + raise ValueError("DB 사용자 컨텍스트에서만 사용자 전용 봇 관리를 수행할 수 있습니다") + return auth_context.user_id + @staticmethod def _build_expiration(expires_in_days: int | None, default_days: int) -> datetime: days = expires_in_days if expires_in_days is not None else default_days diff --git a/docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md b/docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md index a90be43..e3228ec 100644 --- a/docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md +++ b/docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md @@ -15,8 +15,10 @@ - 시스템은 `admin` 권한을 제외한 모든 사용자/토큰 요청에서 `owner_user_id`가 인증 주체와 일치하는 포트폴리오만 조회/변경/삭제 가능해야 한다. - 시스템은 `admin` 권한 요청에서 소유권 제약 없이 포트폴리오 전체를 조회/변경/삭제할 수 있어야 한다. - 시스템은 소유권 불일치로 거부된 요청에 대해 `403 Forbidden`을 반환해야 한다. -- 시스템은 인증 컨텍스트에 사용자 식별자가 없는 토큰이 소유권이 필요한 포트폴리오 작업을 요청하면 `403 Forbidden`을 반환해야 한다. +- 시스템은 봇 principal에 소유 사용자 식별자(`bot_principals.owner_user_id`)를 저장해야 하며, 봇 토큰 인증 시 해당 값을 실효 사용자 식별자로 해석해야 한다. +- 시스템은 사용자 귀속 정보가 해석되지 않는 토큰이 소유권이 필요한 포트폴리오 작업을 요청하면 `403 Forbidden`을 반환해야 한다. - 시스템은 기존 포트폴리오 데이터에 대해 마이그레이션 시 `owner_user_id`를 null 허용으로 추가한 뒤, 백필 완료 후 not null 제약을 적용할 수 있어야 한다. +- 시스템은 기존 봇 principal 데이터에 대해 마이그레이션 시 `bot_principals.owner_user_id`를 null 허용으로 추가한 뒤, 백필 완료 후 not null 및 FK(`users.id`) 제약을 적용할 수 있어야 한다. ## Acceptance Criteria 1. Given 인증된 일반 사용자(또는 해당 사용자에 귀속된 토큰)가 `POST /portfolios`를 호출할 때, When 포트폴리오가 생성되면, Then 저장된 `owner_user_id`는 요청자 사용자 식별자와 동일해야 하며 요청 본문으로 다른 소유주를 지정할 수 없어야 한다. @@ -24,12 +26,13 @@ 3. Given 일반 사용자 A가 목록 조회를 수행할 때, When `GET /portfolios`를 호출하면, Then 응답 목록에는 A 소유 포트폴리오만 포함되어야 한다. 4. Given `admin` 권한 사용자가 존재할 때, When `GET /portfolios` 또는 특정 포트폴리오 변경 API를 호출하면, Then 시스템은 소유권 검사로 요청을 거부하지 않아야 한다. 5. Given 마이그레이션 적용 전 생성된 레거시 포트폴리오가 존재할 때, When ownership 마이그레이션을 적용하면, Then 데이터 손실 없이 `owner_user_id`가 백필되고 이후 읽기/쓰기 API가 실패 없이 동작해야 한다. -6. Given 인증 컨텍스트에 사용자 식별자가 없는 bot 토큰이 포트폴리오 생성/수정/삭제를 요청할 때, When 소유권이 필요한 API를 호출하면, Then 시스템은 `403`을 반환해야 한다. +6. Given 사용자 A에 귀속된 bot 토큰이 존재할 때, When bot 토큰이 `GET /portfolios/{portfolio_id}` 또는 변경 API를 호출하면, Then A 소유 포트폴리오에는 접근 가능하고 A가 아닌 사용자 소유 포트폴리오에는 `403`을 반환해야 한다. +7. Given 인증 컨텍스트에 사용자 식별자가 해석되지 않는 토큰(예: DB 비연결 정적 토큰)이 포트폴리오 생성/수정/삭제를 요청할 때, When 소유권이 필요한 API를 호출하면, Then 시스템은 `403`을 반환해야 한다. ## Impacted Area - Docs: `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md`, `docs/technical/auth-and-token-lifecycle.md`, `docs/technical/api-contracts.md`, `docs/technical/persistence-and-migrations.md`, `docs/technical/portfolio-ownership-and-auth-design.md` - Modules: `app/core/auth.py`, `app/api/portfolios.py`, `app/services/portfolio_service.py`, `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py`, `app/domain/models.py`, `app/schemas/portfolio.py`, `migrations/versions/*` -- Tests: `tests/test_app.py`, `tests/test_auth.py`, `tests/test_cli.py`, `tests/test_domain_services.py` +- Tests: `tests/test_portfolio_ownership.py`, `tests/test_users.py`, `tests/test_auth.py`, `tests/test_portfolio_comments.py` ## Notes - 본 requirement는 인증 서비스 분리 여부와 무관하게 portfolio-hub 내부에서 반드시 충족되어야 하는 리소스 소유권 계약이다. @@ -72,3 +75,39 @@ #### Validation Plan - 사용자별 소유권 격리 통합 테스트, admin 우회 테스트, 마이그레이션 백필 테스트를 통해 계약 충족 여부를 확인한다. + +### CHG-20260414-01 +- Date: 2026-04-14 +- Author: GitHub Copilot + +#### Change Summary +- 봇 principal의 사용자 귀속(`bot_principals.owner_user_id`) 및 봇 토큰 실효 사용자 매핑 규칙을 requirement에 추가했다. + +#### Intent +- 사용자별 봇 bearer token이 해당 사용자 포트폴리오 경계 내에서만 동작하도록 계약을 명확히 하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 사용자 귀속 bot 토큰의 소유권 허용/거부 기준 (AC6) + - 사용자 식별자 미해석 토큰 거부 기준 (AC7) +- Updated: + - Requirement 본문의 bot 토큰 처리 규칙 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` + - `docs/SRS/index.md` + - `docs/SRS/traceability-matrix.md` +- Modules: + - `app/repositories/postgres.py` + - `app/services/auth_service.py` + - `app/schemas/auth.py` + - `migrations/versions/20260414_0011_bot_principal_owner_user_id.py` +- Tests: + - `tests/test_portfolio_ownership.py` + - `tests/test_users.py` + +#### Validation Plan +- 사용자 귀속 bot 토큰의 owner 허용/타 사용자 거부 시나리오와 봇 owner 백필 마이그레이션 적용 검증을 수행한다. diff --git a/docs/SRS/index.md b/docs/SRS/index.md index 27cc8be..bb521e7 100644 --- a/docs/SRS/index.md +++ b/docs/SRS/index.md @@ -27,11 +27,11 @@ | REQ-004 | 포트폴리오 소유주 기반 접근 제어 | Interface | Proposed | High | Justin | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | ## Recent Change Log Summary +- 2026-04-14 — `REQ-004`에 사용자 귀속 봇 principal(`bot_principals.owner_user_id`) 및 봇 토큰 실효 사용자 매핑 규칙(AC6, AC7)을 추가했다. - 2026-04-13 — `REQ-004` 포트폴리오 소유주(`owner_user_id`) 및 소유권 기반 접근 제어 requirement를 추가했다. - 2026-04-13 — `REQ-003`에서 태그 필터 기반 조회의 결정성 유지 책임을 소비 서비스(stock-picker)로 이관했다. - 2026-04-13 — `REQ-003`에서 `sourceVersion`/`sourceTimestamp` 요구를 제거하고 태그 필터 기반 조회 결정성 계약으로 정리했다. - 2026-04-13 — `REQ-003`의 소비자 표현을 특정 서비스 전용에서 태그 기반 조회 소비 서비스 일반 계약으로 정리했다. -- 2026-04-13 — `REQ-002`의 소비자 표현을 특정 서비스 전용에서 다운스트림 소비자 일반 계약으로 정리했다. ## Status Transition Evidence - `Proposed` → `Approved`: Owner가 지정되고 requirement 문서 리뷰가 완료되며 acceptance criteria가 누락 없이 검토되어야 한다. diff --git a/docs/SRS/traceability-matrix.md b/docs/SRS/traceability-matrix.md index 30ff263..b2214fb 100644 --- a/docs/SRS/traceability-matrix.md +++ b/docs/SRS/traceability-matrix.md @@ -14,7 +14,7 @@ | REQ-001 | AC1, AC2, AC3, AC4, AC5, AC6, AC7 | `docs/SRS/Interface/REQ-001_portfolio-comment-history.md` | #2 | TBD | TBD | Approved | In Progress | 2026-04-10 | | REQ-002 | AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9 | `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` | TBD | TBD | TBD | Proposed | Planned | 2026-04-10 | | REQ-003 | AC1, AC2, AC3, AC4, AC5 | `docs/SRS/Data/REQ-003_portfolio-required-fields.md` | TBD | TBD | TBD | Proposed | Planned | 2026-04-10 | -| REQ-004 | AC1, AC2, AC3, AC4, AC5, AC6 | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | TBD | TBD | `tests/test_portfolio_ownership.py`, `tests/test_auth.py`, `tests/test_portfolio_comments.py`, local `pytest` pass (2026-04-14) | Proposed | In Progress | 2026-04-14 | +| REQ-004 | AC1, AC2, AC3, AC4, AC5, AC6, AC7 | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | TBD | TBD | `tests/test_portfolio_ownership.py`, `tests/test_users.py`, `tests/test_auth.py`, `tests/test_portfolio_comments.py`, local `pytest` pass (2026-04-14) | Proposed | In Progress | 2026-04-14 | ## Update Rules - REQ ID must exactly match the SRS requirement file prefix. diff --git a/docs/technical/api-contracts.md b/docs/technical/api-contracts.md index 04dfabf..547efdd 100644 --- a/docs/technical/api-contracts.md +++ b/docs/technical/api-contracts.md @@ -32,10 +32,16 @@ - `DELETE /auth/tokens/{token_id}` - `POST /auth/bots` - `GET /auth/bots` +- `GET /auth/bots/{bot_id}` - `POST /auth/bots/{bot_id}/tokens` - `GET /auth/bots/{bot_id}/tokens` - `DELETE /auth/bots/{bot_id}/tokens/{token_id}` - `DELETE /auth/bots/{bot_id}` +- `POST /auth/me/bots` +- `GET /auth/me/bots` +- `POST /auth/me/bots/{bot_id}/tokens` +- `GET /auth/me/bots/{bot_id}/tokens` +- `DELETE /auth/me/bots/{bot_id}/tokens/{token_id}` ### 포트폴리오 - `GET /portfolios` @@ -427,18 +433,46 @@ curl -X POST "http://localhost:8000/auth/tokens" \ - `GET /auth/bots/{bot_id}/tokens`: admin only, `AuthTokenResponse[]` - `DELETE /auth/bots/{bot_id}/tokens/{token_id}`: admin only, `204` +소유권/권한 제약: + +- bot principal은 `owner_user_id`를 필수로 가진다. +- bot permission은 `read` 또는 `trade-write`만 허용한다. +- bot 토큰 인증 컨텍스트의 실효 `user_id`는 bot owner(`owner_user_id`)로 해석된다. + +#### 사용자 전용(Self-service) 봇 관리 +- `POST /auth/me/bots`: DB-issued `read` 이상 사용자 토큰, `CreateSelfBotPrincipalRequest` -> `BotPrincipalResponse` +- `GET /auth/me/bots`: DB-issued `read` 이상 사용자 토큰, 본인 소유 bot 목록 +- `POST /auth/me/bots/{bot_id}/tokens`: DB-issued `read` 이상 사용자 토큰, `CreateBotTokenRequest` -> `IssuedBotTokenResponse` +- `GET /auth/me/bots/{bot_id}/tokens`: DB-issued `read` 이상 사용자 토큰, `AuthTokenResponse[]` +- `DELETE /auth/me/bots/{bot_id}/tokens/{token_id}`: DB-issued `read` 이상 사용자 토큰, `204` + +제약: + +- `/auth/me/bots*` 경로는 요청자 본인 소유 bot(`owner_user_id == auth_context.user_id`)에만 허용된다. +- 타 사용자 소유 bot 접근 시 `403`. + 대표 요청: ```bash curl -X POST "http://localhost:8000/auth/bots" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ - -d '{"name": "rebalance-bot", "permission": "trade-write", "description": "nightly jobs"}' + -d '{"name": "rebalance-bot", "owner_user_id": "user-1", "permission": "trade-write", "description": "nightly jobs"}' curl -X POST "http://localhost:8000/auth/bots/bot-1/tokens" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"name": "nightly", "permission": "trade-write", "expires_in_days": 90}' + +curl -X POST "http://localhost:8000/auth/me/bots" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"name": "my-bot", "permission": "read", "description": "personal automation"}' + +curl -X POST "http://localhost:8000/auth/me/bots/bot-1/tokens" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"name": "personal-key", "permission": "read", "expires_in_days": 30}' ``` ### 사용자 @@ -543,13 +577,20 @@ curl -X POST "http://localhost:8000/users" \ `CreateBotPrincipalRequest` - `name: str` -- `permission: "read" | "trade-write" | "admin"` +- `owner_user_id: str` +- `permission: "read" | "trade-write"` +- `description: str | null` + +`CreateSelfBotPrincipalRequest` + +- `name: str` +- `permission: "read" | "trade-write"` - `description: str | null` `CreateBotTokenRequest` - `name: str` -- `permission: "read" | "trade-write" | "admin" | null` +- `permission: "read" | "trade-write" | null` - `expires_in_days: int | null` `CreateUserRequest` @@ -628,7 +669,8 @@ curl -X POST "http://localhost:8000/users" \ - `id: str` - `name: str` -- `permission: "read" | "trade-write" | "admin"` +- `owner_user_id: str` +- `permission: "read" | "trade-write"` - `description: str | null` - `is_active: bool` - `created_at: datetime | null` diff --git a/docs/technical/auth-and-token-lifecycle.md b/docs/technical/auth-and-token-lifecycle.md index 1f96072..48160e2 100644 --- a/docs/technical/auth-and-token-lifecycle.md +++ b/docs/technical/auth-and-token-lifecycle.md @@ -33,6 +33,31 @@ - 환경변수 기반 정적 토큰: `PORTFOLIOHUB_*_TOKENS` - DB 저장 토큰: 로그인 세션/클라이언트 토큰/봇 토큰 +봇 토큰 사용자 컨텍스트 정책: + +- 봇 principal은 `owner_user_id`를 가진다. +- 봇 토큰 인증 시 실효 사용자 식별자는 `bot_principals.owner_user_id`로 해석한다. +- 따라서 소유권이 필요한 포트폴리오 API에서 봇 토큰은 소유 사용자 경계 내에서만 동작한다. + +## 봇 관리 API 정책 + +- Admin 경로: + - `POST /auth/bots` + - `GET /auth/bots` + - `GET /auth/bots/{bot_id}` + - `DELETE /auth/bots/{bot_id}` + - `POST/GET/DELETE /auth/bots/{bot_id}/tokens*` +- Self-service 경로(사용자 본인 전용): + - `POST /auth/me/bots` + - `GET /auth/me/bots` + - `POST/GET/DELETE /auth/me/bots/{bot_id}/tokens*` + +제약: + +- self-service 경로는 DB 사용자 컨텍스트가 있는 사용자 토큰에서만 허용한다. +- self-service 경로는 요청자의 소유 봇(`owner_user_id == auth_context.user_id`)에만 허용한다. +- 봇 권한은 `read` 또는 `trade-write`만 허용한다. + ## 토큰 라이프사이클 1. 사용자 로그인(`POST /auth/login`) 또는 토큰 발급 API 호출 2. 토큰 해시 저장 및 prefix 관리 diff --git a/docs/technical/portfolio-ownership-and-auth-design.md b/docs/technical/portfolio-ownership-and-auth-design.md index 9add3f2..e5b5ea5 100644 --- a/docs/technical/portfolio-ownership-and-auth-design.md +++ b/docs/technical/portfolio-ownership-and-auth-design.md @@ -75,6 +75,7 @@ FOREIGN KEY (owner_user_id) REFERENCES users(id); - 2단계: admin이 아니면 ownership 검사 수행 - ownership 검사 기준: `portfolio.owner_user_id == auth_context.user_id` - `auth_context.user_id`가 없는 요청은 ownership 필요한 엔드포인트에서 `403` +- 봇 토큰(`token_type=bot`)의 `auth_context.user_id`는 `bot_principals.owner_user_id`로 해석 ## Endpoint Policy Matrix @@ -92,6 +93,16 @@ FOREIGN KEY (owner_user_id) REFERENCES users(id); | `DELETE /portfolios/{portfolio_id}` | admin | admin only | 기존 정책 유지 | | `POST/GET/PATCH/DELETE /portfolios/{id}/comments*` | read or trade-write | 적용 | 상위 포트폴리오 ownership을 재사용 | +## Bot Principal Ownership 정책 + +- `bot_principals.owner_user_id`는 필수이며 `users.id` FK를 가진다. +- 봇 principal 생성 정책: + - Admin: 임의 사용자 owner로 생성 가능 + - Self-service 사용자: 본인 owner로만 생성 가능 +- 봇 토큰 관리 정책: + - Admin 경로는 전체 봇 대상으로 관리 가능 + - Self-service 경로는 본인 소유 봇 대상으로만 관리 가능 + ## 서비스/리포지토리 반영 포인트 - Domain model: - `Portfolio`에 `owner_user_id` 추가 @@ -116,13 +127,16 @@ FOREIGN KEY (owner_user_id) REFERENCES users(id); 1. 기존 portfolios row가 존재할 때 owner_user_id 백필 후 null row가 0건인지 검증 2. users가 없는 상태에서 migration 실행 시 실패 동작 검증 3. downgrade 시 인덱스/제약 해제가 순서대로 수행되는지 검증 +4. 기존 bot_principals row가 존재할 때 owner_user_id 백필 후 null row가 0건인지 검증 +5. bot_principals.owner_user_id FK(`users.id`) 제약 생성/해제 검증 ## API Authorization 1. 사용자 A 소유 portfolio를 사용자 B가 조회하면 403 2. 사용자 A 소유 portfolio를 사용자 B가 수정/매매/리셋하면 403 3. admin이 타 사용자 portfolio 조회/수정 가능 4. 일반 사용자의 `GET /portfolios`는 본인 소유만 반환 -5. user_id 없는 bot principal 요청이 ownership 필요 API 호출 시 403 +5. 사용자 A에 귀속된 bot 토큰은 A 소유 portfolio만 접근 가능 +6. 사용자 B가 A 소유 bot의 self-service 토큰 발급/조회/폐기를 시도하면 403 ## API Creation 1. `POST /portfolios`에서 owner_user_id가 인증 사용자로 저장되는지 검증 diff --git a/migrations/versions/20260414_0011_bot_principal_owner_user_id.py b/migrations/versions/20260414_0011_bot_principal_owner_user_id.py new file mode 100644 index 0000000..890c35d --- /dev/null +++ b/migrations/versions/20260414_0011_bot_principal_owner_user_id.py @@ -0,0 +1,97 @@ +"""restore bot principal owner user id + +Revision ID: 20260414_0011 +Revises: 20260413_0010 +Create Date: 2026-04-14 10:30:00 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +from sqlalchemy import inspect + + +revision = "20260414_0011" +down_revision = "20260413_0010" +branch_labels = None +depends_on = None + + +def _column_exists(inspector, table_name: str, column_name: str) -> bool: + return any(column["name"] == column_name for column in inspector.get_columns(table_name)) + + +def _index_exists(inspector, table_name: str, index_name: str) -> bool: + return any(index["name"] == index_name for index in inspector.get_indexes(table_name)) + + +def _fk_exists(inspector, table_name: str, fk_name: str) -> bool: + return any(fk.get("name") == fk_name for fk in inspector.get_foreign_keys(table_name)) + + +def upgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + + if not _column_exists(inspector, "bot_principals", "owner_user_id"): + op.add_column("bot_principals", sa.Column("owner_user_id", sa.Text(), nullable=True)) + + bind.execute( + sa.text( + """ + WITH fallback_owner AS ( + SELECT id + FROM users + ORDER BY created_at ASC, id ASC + LIMIT 1 + ) + UPDATE bot_principals b + SET owner_user_id = f.id + FROM fallback_owner f + WHERE b.owner_user_id IS NULL + """ + ) + ) + + unresolved = bind.execute(sa.text("SELECT COUNT(*) FROM bot_principals WHERE owner_user_id IS NULL")).scalar() + if unresolved and int(unresolved) > 0: + raise RuntimeError("bot_principals.owner_user_id backfill failed: users 테이블에 fallback 소유주가 필요합니다") + + op.alter_column("bot_principals", "owner_user_id", existing_type=sa.Text(), nullable=False) + + inspector = inspect(bind) + if not _index_exists(inspector, "bot_principals", "idx_bot_principals_owner_active_created"): + op.create_index( + "idx_bot_principals_owner_active_created", + "bot_principals", + ["owner_user_id", "is_active", sa.text("created_at DESC")], + unique=False, + ) + + inspector = inspect(bind) + if not _fk_exists(inspector, "bot_principals", "fk_bot_principals_owner_user"): + op.create_foreign_key( + "fk_bot_principals_owner_user", + "bot_principals", + "users", + ["owner_user_id"], + ["id"], + ondelete="RESTRICT", + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + + if _fk_exists(inspector, "bot_principals", "fk_bot_principals_owner_user"): + op.drop_constraint("fk_bot_principals_owner_user", "bot_principals", type_="foreignkey") + + inspector = inspect(bind) + if _index_exists(inspector, "bot_principals", "idx_bot_principals_owner_active_created"): + op.drop_index("idx_bot_principals_owner_active_created", table_name="bot_principals") + + inspector = inspect(bind) + if _column_exists(inspector, "bot_principals", "owner_user_id"): + op.drop_column("bot_principals", "owner_user_id") diff --git a/tests/test_portfolio_comments.py b/tests/test_portfolio_comments.py index 8e56989..be90297 100644 --- a/tests/test_portfolio_comments.py +++ b/tests/test_portfolio_comments.py @@ -119,7 +119,7 @@ class FakeRepo: def get_bot_principal(self, bot_principal_id: str): raise ValueError("봇을 찾을 수 없습니다") - def list_bot_principals(self, *, is_active=None): + def list_bot_principals(self, *, is_active=None, owner_user_id=None): return [] def deactivate_bot_principal(self, *args, **kwargs): diff --git a/tests/test_portfolio_ownership.py b/tests/test_portfolio_ownership.py index a66fe90..f346a1d 100644 --- a/tests/test_portfolio_ownership.py +++ b/tests/test_portfolio_ownership.py @@ -108,6 +108,25 @@ def test_non_owner_gets_403_on_db_auth(monkeypatch) -> None: assert response.json()["detail"] == "Portfolio ownership mismatch" +def test_owner_bound_bot_token_can_read_owned_portfolio(monkeypatch) -> None: + token = "owner-bot-token" + principal = TokenPrincipal( + token_id="t-bot-1", + user_id="owner-1", + permission="read", + token_type="bot", + bot_principal_id="bot-1", + ) + repo = FakeRepo(portfolio=_portfolio("owner-1"), principals={hash_token(token): principal}) + client = _configure_client(monkeypatch, repo) + + response = client.get("/portfolios/p1", headers={"Authorization": f"Bearer {token}"}) + + app.dependency_overrides.clear() + assert response.status_code == 200 + assert response.json()["portfolio"]["owner_user_id"] == "owner-1" + + def test_db_enabled_env_token_without_user_context_is_rejected(monkeypatch) -> None: repo = FakeRepo(portfolio=_portfolio("owner-1"), principals={}) monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "true") diff --git a/tests/test_users.py b/tests/test_users.py index 72cd094..c97c108 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -163,21 +163,33 @@ class FakeUserRepo: if token.expires_at is not None and token.expires_at <= datetime.now(timezone.utc): return None token.last_used_at = datetime.now(timezone.utc) + if token.bot_principal_id is not None: + bot = self.get_bot_principal(token.bot_principal_id) + return TokenPrincipal( + token_id=principal.token_id, + user_id=bot.owner_user_id, + permission=principal.permission, + token_type=principal.token_type, + bot_principal_id=principal.bot_principal_id, + ) return principal def create_bot_principal( self, *, name: str, + owner_user_id: str, permission: str, description: str | None = None, ) -> BotPrincipal: if any(item.name == name for item in self.bots.values()): raise ValueError(f"이미 존재하는 bot name입니다: {name}") + self.get_user(owner_user_id) now = datetime.now(timezone.utc) bot = BotPrincipal( id=f"bot-{self.next_bot_id}", name=name, + owner_user_id=owner_user_id, permission=permission, description=description, is_active=True, @@ -194,10 +206,17 @@ class FakeUserRepo: raise ValueError("봇을 찾을 수 없습니다") return bot - def list_bot_principals(self, *, is_active: bool | None = None) -> list[BotPrincipal]: + def list_bot_principals( + self, + *, + is_active: bool | None = None, + owner_user_id: str | None = None, + ) -> list[BotPrincipal]: items = list(self.bots.values()) if is_active is not None: items = [item for item in items if item.is_active == is_active] + if owner_user_id is not None: + items = [item for item in items if item.owner_user_id == owner_user_id] return sorted(items, key=lambda item: item.name) def deactivate_bot_principal(self, bot_principal_id: str) -> BotPrincipal: @@ -340,7 +359,12 @@ def test_admin_can_create_bot_and_issue_token(monkeypatch) -> None: bot_response = client.post( "/auth/bots", headers={"Authorization": f"Bearer {admin_token}"}, - json={"name": "market-bot", "permission": "trade-write", "description": "external execution bot"}, + json={ + "name": "market-bot", + "owner_user_id": admin.id, + "permission": "trade-write", + "description": "external execution bot", + }, ) assert bot_response.status_code == 201 bot_id = bot_response.json()["id"] @@ -373,7 +397,7 @@ def test_deactivated_bot_token_is_rejected(monkeypatch) -> None: bot_response = client.post( "/auth/bots", headers={"Authorization": f"Bearer {admin_token}"}, - json={"name": "ops-bot", "permission": "read"}, + json={"name": "ops-bot", "owner_user_id": admin.id, "permission": "read"}, ) assert bot_response.status_code == 201 bot_id = bot_response.json()["id"] @@ -396,4 +420,61 @@ def test_deactivated_bot_token_is_rejected(monkeypatch) -> None: app.dependency_overrides.clear() assert denied_response.status_code == 401 - assert denied_response.json()["detail"] == "Invalid token" \ No newline at end of file + assert denied_response.json()["detail"] == "Invalid token" + + +def test_user_can_create_and_issue_owned_bot_token(monkeypatch) -> None: + repo = FakeUserRepo() + user = repo.create_user("alice", password_hash=hash_password("alice123!"), permission="trade-write") + client = _client_with_repo(monkeypatch, repo) + + login_response = client.post("/auth/login", json={"username": "alice", "password": "alice123!"}) + assert login_response.status_code == 200 + user_token = login_response.json()["access_token"] + + bot_response = client.post( + "/auth/me/bots", + headers={"Authorization": f"Bearer {user_token}"}, + json={"name": "alice-bot", "permission": "read", "description": "personal bot"}, + ) + assert bot_response.status_code == 201 + bot_id = bot_response.json()["id"] + assert bot_response.json()["owner_user_id"] == user.id + + list_response = client.get("/auth/me/bots", headers={"Authorization": f"Bearer {user_token}"}) + assert list_response.status_code == 200 + assert [item["id"] for item in list_response.json()] == [bot_id] + + issue_response = client.post( + f"/auth/me/bots/{bot_id}/tokens", + headers={"Authorization": f"Bearer {user_token}"}, + json={"name": "alice-key", "permission": "read"}, + ) + + app.dependency_overrides.clear() + assert issue_response.status_code == 201 + payload = issue_response.json() + assert payload["auth_token"]["token_type"] == "bot" + assert payload["auth_token"]["bot_principal_id"] == bot_id + + +def test_user_cannot_issue_token_for_other_users_bot(monkeypatch) -> None: + repo = FakeUserRepo() + owner = repo.create_user("owner", password_hash=hash_password("owner123!"), permission="trade-write") + other = repo.create_user("other", password_hash=hash_password("other123!"), permission="trade-write") + bot = repo.create_bot_principal(name="owner-bot", owner_user_id=owner.id, permission="read") + client = _client_with_repo(monkeypatch, repo) + + login_response = client.post("/auth/login", json={"username": "other", "password": "other123!"}) + assert login_response.status_code == 200 + other_token = login_response.json()["access_token"] + + issue_response = client.post( + f"/auth/me/bots/{bot.id}/tokens", + headers={"Authorization": f"Bearer {other_token}"}, + json={"name": "invalid-key", "permission": "read"}, + ) + + app.dependency_overrides.clear() + assert issue_response.status_code == 403 + assert issue_response.json()["detail"] == "봇 접근 권한이 없습니다" \ No newline at end of file From a776ea3b1d3cb30822e1a6a60c669f9f9af26b4e Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 01:06:10 +0900 Subject: [PATCH 07/21] feat(req-002): implement portfolio tagging and tag filters --- app/api/portfolios.py | 80 +++++++- app/cli/commands/portfolio.py | 59 +++++- app/domain/models.py | 8 + app/repositories/portfolio_repository.py | 11 +- app/repositories/postgres.py | 140 ++++++++++++- app/schemas/portfolio.py | 21 +- app/services/portfolio_service.py | 58 +++++- .../REQ-002_portfolio-tagging-and-search.md | 85 +++++++- docs/SRS/index.md | 6 +- docs/SRS/traceability-matrix.md | 2 +- docs/technical/api-contracts.md | 43 +++- docs/technical/index.md | 3 +- docs/technical/persistence-and-migrations.md | 48 ++++- .../portfolio-tagging-and-search-design.md | 109 ++++++++++ .../versions/20260414_0012_portfolio_tags.py | 79 ++++++++ tests/test_cli.py | 80 +++++++- tests/test_domain_services.py | 12 ++ tests/test_portfolio_comments.py | 11 +- tests/test_portfolio_ownership.py | 23 +++ tests/test_portfolio_tags.py | 187 ++++++++++++++++++ 20 files changed, 1033 insertions(+), 32 deletions(-) create mode 100644 docs/technical/portfolio-tagging-and-search-design.md create mode 100644 migrations/versions/20260414_0012_portfolio_tags.py create mode 100644 tests/test_portfolio_tags.py diff --git a/app/api/portfolios.py b/app/api/portfolios.py index be392b5..b03ec25 100644 --- a/app/api/portfolios.py +++ b/app/api/portfolios.py @@ -1,6 +1,6 @@ """Portfolio API routes.""" -from fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request, Response, status from app.core.auth import AuthContext, Permission, authorize_request from app.core.config import Settings, get_settings @@ -14,6 +14,8 @@ from app.schemas.portfolio import ( InvestRequest, PortfolioDetailResponse, PortfolioListItem, + PortfolioTagMutationRequest, + PortfolioTagResponse, ResetPortfolioRequest, SellRequest, TransferRequest, @@ -26,6 +28,8 @@ router = APIRouter(prefix="/portfolios", tags=["portfolios"]) def _raise_portfolio_error(exc: ValueError) -> None: detail = str(exc) + if "이미 연결된 태그입니다" in detail: + raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=detail) from exc if "찾을 수 없습니다" in detail: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail) from exc raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail) from exc @@ -68,6 +72,9 @@ def list_portfolios( request: Request, broker: str | None = None, status_filter: str | None = None, + include_tags: list[str] | None = Query(default=None, alias="includeTags"), + exclude_tags: list[str] | None = Query(default=None, alias="excludeTags"), + match_mode: str | None = Query(default=None, alias="matchMode"), settings: Settings = Depends(get_settings), ) -> list[PortfolioListItem]: auth_context = authorize_request(Permission.READ, request, settings) @@ -76,7 +83,16 @@ def list_portfolios( owner_user_id: str | None = None if _ownership_required(settings) and auth_context.permission != Permission.ADMIN: owner_user_id = _require_owner_context(auth_context) - return service.list_portfolios(broker=broker, status=status_filter, owner_user_id=owner_user_id) + return service.list_portfolios( + broker=broker, + status=status_filter, + owner_user_id=owner_user_id, + include_tags=include_tags, + exclude_tags=exclude_tags, + match_mode=match_mode, + ) + except ValueError as exc: + _raise_portfolio_error(exc) finally: _close_repository(repository) @@ -329,3 +345,63 @@ def delete_comment( finally: _close_repository(repository) return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.post("/{portfolio_id}/tags", response_model=PortfolioTagResponse, status_code=status.HTTP_201_CREATED) +def add_tag( + portfolio_id: str, + payload: PortfolioTagMutationRequest, + request: Request, + *, + settings: Settings = Depends(get_settings), +) -> PortfolioTagResponse: + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) + service, repository = _get_service(settings) + try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) + return service.add_tag(portfolio_id, payload) + except ValueError as exc: + _raise_portfolio_error(exc) + finally: + _close_repository(repository) + + +@router.get("/{portfolio_id}/tags", response_model=list[PortfolioTagResponse]) +def list_tags( + portfolio_id: str, + request: Request, + *, + settings: Settings = Depends(get_settings), +) -> list[PortfolioTagResponse]: + auth_context = authorize_request(Permission.READ, request, settings) + service, repository = _get_service(settings) + try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) + return service.list_tags(portfolio_id) + except ValueError as exc: + _raise_portfolio_error(exc) + finally: + _close_repository(repository) + + +@router.delete("/{portfolio_id}/tags", status_code=status.HTTP_204_NO_CONTENT) +def remove_tag( + portfolio_id: str, + request: Request, + payload: PortfolioTagMutationRequest = Body(...), + *, + settings: Settings = Depends(get_settings), +) -> Response: + auth_context = authorize_request(Permission.TRADE_WRITE, request, settings) + service, repository = _get_service(settings) + try: + if _ownership_required(settings): + _ensure_portfolio_owned(repository, portfolio_id, auth_context) + service.remove_tag(portfolio_id, payload) + except ValueError as exc: + _raise_portfolio_error(exc) + finally: + _close_repository(repository) + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/app/cli/commands/portfolio.py b/app/cli/commands/portfolio.py index 01936dd..b599037 100644 --- a/app/cli/commands/portfolio.py +++ b/app/cli/commands/portfolio.py @@ -14,6 +14,7 @@ from app.schemas.portfolio import ( CommentUpdateRequest, CreatePortfolioRequest, InvestRequest, + PortfolioTagMutationRequest, ResetPortfolioRequest, SellRequest, TransferRequest, @@ -24,7 +25,9 @@ from app.services.portfolio_service import PortfolioService portfolio_app = typer.Typer(help="Portfolio lifecycle and trade commands.") comment_app = typer.Typer(help="Portfolio comment history commands.") +tag_app = typer.Typer(help="Portfolio tag commands.") portfolio_app.add_typer(comment_app, name="comment") +portfolio_app.add_typer(tag_app, name="tag") def _parse_strategy_params(value: str) -> dict[str, Any]: @@ -61,8 +64,20 @@ def list_portfolios( ctx: typer.Context, broker: str | None = typer.Option(None, help="Filter by broker."), status: str | None = typer.Option(None, help="Filter by status."), + include_tags: list[str] | None = typer.Option(None, "--include-tag", help="Include tag filter. Repeatable."), + exclude_tags: list[str] | None = typer.Option(None, "--exclude-tag", help="Exclude tag filter. Repeatable."), + match_mode: str | None = typer.Option(None, "--match-mode", help="AND or OR for include tags."), ) -> None: - detail = _with_service(ctx, lambda service, app_context: service.list_portfolios(broker=broker, status=status)) + detail = _with_service( + ctx, + lambda service, app_context: service.list_portfolios( + broker=broker, + status=status, + include_tags=include_tags, + exclude_tags=exclude_tags, + match_mode=match_mode, + ), + ) render_output(ctx.obj.console, ctx.obj.output_format, detail, empty_message="No portfolios found.") @@ -333,4 +348,44 @@ def comment_delete( if ctx.obj.output_format == "json": render_output(ctx.obj.console, ctx.obj.output_format, {"deleted": comment_id, "portfolio_id": portfolio_id}) else: - ctx.obj.console.print(f"Deleted comment: {comment_id}") \ No newline at end of file + ctx.obj.console.print(f"Deleted comment: {comment_id}") + + +@tag_app.command("add") +def tag_add( + ctx: typer.Context, + portfolio_id: str = typer.Argument(..., help="Portfolio identifier."), + tag: str = typer.Option(..., "--tag", help="Tag value."), +) -> None: + detail = _with_service( + ctx, + lambda service, app_context: service.add_tag(portfolio_id, PortfolioTagMutationRequest(tag=tag)), + ) + render_output(ctx.obj.console, ctx.obj.output_format, detail) + + +@tag_app.command("list") +def tag_list( + ctx: typer.Context, + portfolio_id: str = typer.Argument(..., help="Portfolio identifier."), +) -> None: + detail = _with_service(ctx, lambda service, app_context: service.list_tags(portfolio_id)) + render_output(ctx.obj.console, ctx.obj.output_format, detail, empty_message="No tags found.") + + +@tag_app.command("remove") +def tag_remove( + ctx: typer.Context, + portfolio_id: str = typer.Argument(..., help="Portfolio identifier."), + tag: str = typer.Option(..., "--tag", help="Tag value."), + force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."), +) -> None: + _confirm(force, f"Remove tag {tag!r} from portfolio {portfolio_id}?") + _with_service( + ctx, + lambda service, app_context: service.remove_tag(portfolio_id, PortfolioTagMutationRequest(tag=tag)), + ) + if ctx.obj.output_format == "json": + render_output(ctx.obj.console, ctx.obj.output_format, {"portfolio_id": portfolio_id, "deleted": tag.strip()}) + else: + ctx.obj.console.print(f"Removed tag: {tag.strip()}") \ No newline at end of file diff --git a/app/domain/models.py b/app/domain/models.py index a0030ae..ef49473 100644 --- a/app/domain/models.py +++ b/app/domain/models.py @@ -73,6 +73,7 @@ class Portfolio: status: str name: str symbol: str + tags: list[str] | None = None owner_user_id: str | None = None cash_balance: float = 0.0 asset_quantity: float = 0.0 @@ -111,6 +112,13 @@ class PortfolioComment: updated_at: datetime | None = None +@dataclass +class PortfolioTag: + portfolio_id: str + tag: str + created_at: datetime | None = None + + @runtime_checkable class LiveAssetSource(Protocol): def get_asset_quantity(self, symbol: str) -> float: ... diff --git a/app/repositories/portfolio_repository.py b/app/repositories/portfolio_repository.py index 18962a0..4d6c932 100644 --- a/app/repositories/portfolio_repository.py +++ b/app/repositories/portfolio_repository.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime from typing import Protocol -from app.domain.models import AuthToken, BotPrincipal, LedgerEntry, Portfolio, PortfolioComment, StrategySet, TokenPrincipal, User +from app.domain.models import AuthToken, BotPrincipal, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, TokenPrincipal, User class PortfolioRepository(Protocol): @@ -126,8 +126,17 @@ class PortfolioRepository(Protocol): broker: str | None = None, status: str | None = None, owner_user_id: str | None = None, + include_tags: list[str] | None = None, + exclude_tags: list[str] | None = None, + match_mode: str | None = None, ) -> list[Portfolio]: ... + def add_tag(self, portfolio_id: str, *, tag: str) -> PortfolioTag: ... + + def list_tags(self, portfolio_id: str) -> list[PortfolioTag]: ... + + def remove_tag(self, portfolio_id: str, *, tag: str) -> None: ... + def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: ... def get_strategy_set(self, id: str) -> StrategySet | None: ... diff --git a/app/repositories/postgres.py b/app/repositories/postgres.py index a0a10b6..5ecd26a 100644 --- a/app/repositories/postgres.py +++ b/app/repositories/postgres.py @@ -12,7 +12,7 @@ from typing import Any import psycopg from psycopg import sql -from app.domain.models import AuthToken, BotPrincipal, LedgerEntry, Portfolio, PortfolioComment, StrategySet, TokenPrincipal, User +from app.domain.models import AuthToken, BotPrincipal, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, TokenPrincipal, User _SELECT_USER = """ SELECT @@ -329,6 +329,22 @@ class PostgresPortfolioRepository: ON portfolio_comments (portfolio_id, created_at DESC) """ ) + cur.execute( + """ + CREATE TABLE IF NOT EXISTS portfolio_tags ( + portfolio_id TEXT NOT NULL, + tag_value TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (portfolio_id, tag_value) + ) + """ + ) + cur.execute( + """ + CREATE INDEX IF NOT EXISTS idx_portfolio_tags_tag_portfolio + ON portfolio_tags (tag_value, portfolio_id) + """ + ) cur.execute("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb')") if cur.fetchone()[0]: cur.execute( @@ -407,6 +423,7 @@ class PostgresPortfolioRepository: status=row[2] or "enabled", name=row[3], symbol=row[4], + tags=None, cash_balance=float(row[5]), asset_quantity=float(row[6]), price=float(row[7]), @@ -419,6 +436,35 @@ class PostgresPortfolioRepository: last_trade_at=row[14] if len(row) > 14 else None, ) + @staticmethod + def _row_to_portfolio_tag(row: Any) -> PortfolioTag: + if row is None: + raise ValueError("태그를 찾을 수 없습니다") + return PortfolioTag(portfolio_id=row[0], tag=row[1], created_at=row[2] if len(row) > 2 else None) + + def _load_tags_for_portfolios(self, cur: psycopg.Cursor, portfolio_ids: list[str]) -> dict[str, list[str]]: + if not portfolio_ids: + return {} + cur.execute( + """ + SELECT portfolio_id, tag_value + FROM portfolio_tags + WHERE portfolio_id = ANY(%s) + ORDER BY tag_value ASC + """, + (portfolio_ids,), + ) + tags_by_portfolio: dict[str, list[str]] = {portfolio_id: [] for portfolio_id in portfolio_ids} + for portfolio_id, tag_value in cur.fetchall(): + tags_by_portfolio.setdefault(str(portfolio_id), []).append(str(tag_value)) + return tags_by_portfolio + + def _attach_tags(self, cur: psycopg.Cursor, portfolios: list[Portfolio]) -> list[Portfolio]: + tags_by_portfolio = self._load_tags_for_portfolios(cur, [portfolio.portfolio_id for portfolio in portfolios]) + for portfolio in portfolios: + portfolio.tags = tags_by_portfolio.get(portfolio.portfolio_id, []) + return portfolios + @staticmethod def _row_to_ledger_entry(row: Any) -> LedgerEntry: return LedgerEntry( @@ -780,7 +826,9 @@ class PostgresPortfolioRepository: with self._connect().cursor() as cur: cur.execute(_SELECT_PORTFOLIO + "WHERE portfolio_id = %s", (portfolio_id,)) row = cur.fetchone() - return self._row_to_portfolio(row) + portfolio = self._row_to_portfolio(row) + self._attach_tags(cur, [portfolio]) + return portfolio def get_or_create( self, @@ -875,7 +923,9 @@ class PostgresPortfolioRepository: ), ) row = cur.fetchone() - return self._row_to_portfolio(row) + portfolio = self._row_to_portfolio(row) + portfolio.tags = [] + return portfolio def update_portfolio(self, portfolio_id: str, **fields: object) -> Portfolio: self.ensure_schema() @@ -925,6 +975,9 @@ class PostgresPortfolioRepository: broker: str | None = None, status: str | None = None, owner_user_id: str | None = None, + include_tags: list[str] | None = None, + exclude_tags: list[str] | None = None, + match_mode: str | None = None, ) -> list[Portfolio]: self.ensure_schema() query = _SELECT_PORTFOLIO @@ -939,13 +992,92 @@ class PostgresPortfolioRepository: if owner_user_id is not None: clauses.append("owner_user_id = %s") params.append(owner_user_id) + normalized_mode = (match_mode or "OR").upper() + if include_tags: + if normalized_mode == "AND": + clauses.append( + """ + portfolio_id IN ( + SELECT portfolio_id + FROM portfolio_tags + WHERE tag_value = ANY(%s) + GROUP BY portfolio_id + HAVING COUNT(DISTINCT tag_value) = %s + ) + """ + ) + params.append(include_tags) + params.append(len(set(include_tags))) + else: + clauses.append( + "portfolio_id IN (SELECT DISTINCT portfolio_id FROM portfolio_tags WHERE tag_value = ANY(%s))" + ) + params.append(include_tags) + if exclude_tags: + clauses.append( + "portfolio_id NOT IN (SELECT DISTINCT portfolio_id FROM portfolio_tags WHERE tag_value = ANY(%s))" + ) + params.append(exclude_tags) if clauses: query += "WHERE " + " AND ".join(clauses) + " " query += "ORDER BY updated_at DESC, portfolio_id ASC" with self._connect().cursor() as cur: cur.execute(query, tuple(params)) rows = cur.fetchall() - return [self._row_to_portfolio(row) for row in rows] + portfolios = [self._row_to_portfolio(row) for row in rows] + self._attach_tags(cur, portfolios) + return portfolios + + def add_tag(self, portfolio_id: str, *, tag: str) -> PortfolioTag: + self.ensure_schema() + with self._connect().cursor() as cur: + cur.execute("SELECT 1 FROM portfolios WHERE portfolio_id = %s", (portfolio_id,)) + if cur.fetchone() is None: + raise ValueError("포트폴리오를 찾을 수 없습니다") + try: + cur.execute( + """ + INSERT INTO portfolio_tags (portfolio_id, tag_value) + VALUES (%s, %s) + RETURNING portfolio_id, tag_value, created_at + """, + (portfolio_id, tag), + ) + except psycopg.errors.UniqueViolation as exc: + raise ValueError(f"이미 연결된 태그입니다: {tag}") from exc + row = cur.fetchone() + return self._row_to_portfolio_tag(row) + + def list_tags(self, portfolio_id: str) -> list[PortfolioTag]: + self.ensure_schema() + with self._connect().cursor() as cur: + cur.execute("SELECT 1 FROM portfolios WHERE portfolio_id = %s", (portfolio_id,)) + if cur.fetchone() is None: + raise ValueError("포트폴리오를 찾을 수 없습니다") + cur.execute( + """ + SELECT portfolio_id, tag_value, created_at + FROM portfolio_tags + WHERE portfolio_id = %s + ORDER BY tag_value ASC + """, + (portfolio_id,), + ) + rows = cur.fetchall() + return [self._row_to_portfolio_tag(row) for row in rows] + + def remove_tag(self, portfolio_id: str, *, tag: str) -> None: + self.ensure_schema() + with self._connect().cursor() as cur: + cur.execute("SELECT 1 FROM portfolios WHERE portfolio_id = %s", (portfolio_id,)) + if cur.fetchone() is None: + raise ValueError("포트폴리오를 찾을 수 없습니다") + cur.execute( + "DELETE FROM portfolio_tags WHERE portfolio_id = %s AND tag_value = %s RETURNING portfolio_id", + (portfolio_id, tag), + ) + if cur.fetchone() is None: + raise ValueError("태그를 찾을 수 없습니다") def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: self.ensure_schema() diff --git a/app/schemas/portfolio.py b/app/schemas/portfolio.py index 12fb573..83add30 100644 --- a/app/schemas/portfolio.py +++ b/app/schemas/portfolio.py @@ -5,7 +5,7 @@ from __future__ import annotations from datetime import datetime from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator class StrategySetResponse(BaseModel): @@ -45,6 +45,7 @@ class PortfolioSummary(BaseModel): status: str name: str symbol: str + tags: list[str] = Field(default_factory=list) owner_user_id: str | None = None cash_balance: float = 0.0 asset_quantity: float = 0.0 @@ -140,6 +141,24 @@ class CommentResponse(BaseModel): updated_at: datetime | None = None +class PortfolioTagMutationRequest(BaseModel): + tag: str = Field(min_length=1) + + @field_validator("tag") + @classmethod + def normalize_tag(cls, value: str) -> str: + normalized = value.strip() + if not normalized: + raise ValueError("tag는 비어 있을 수 없습니다") + return normalized + + +class PortfolioTagResponse(BaseModel): + portfolio_id: str + tag: str + created_at: datetime | None = None + + class ErrorResponse(BaseModel): detail: str model_config = ConfigDict(extra="ignore") diff --git a/app/services/portfolio_service.py b/app/services/portfolio_service.py index 3f04d6e..d880049 100644 --- a/app/services/portfolio_service.py +++ b/app/services/portfolio_service.py @@ -7,7 +7,7 @@ import json from fastapi import Depends from app.core.config import Settings, get_settings -from app.domain.models import LedgerEntry, Portfolio, PortfolioComment, StrategySet +from app.domain.models import LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet from app.domain.services import ( apply_buy, apply_delete, @@ -28,6 +28,8 @@ from app.schemas.portfolio import ( InvestRequest, PortfolioDetailResponse, PortfolioListItem, + PortfolioTagMutationRequest, + PortfolioTagResponse, ResetPortfolioRequest, SellRequest, TransferRequest, @@ -49,6 +51,7 @@ def _to_portfolio_summary(portfolio: Portfolio): status=portfolio.status, name=portfolio.name, symbol=portfolio.symbol, + tags=list(portfolio.tags or []), owner_user_id=portfolio.owner_user_id, cash_balance=portfolio.cash_balance, asset_quantity=portfolio.asset_quantity, @@ -121,6 +124,31 @@ def _to_comment_response(comment: PortfolioComment) -> CommentResponse: ) +def _to_tag_response(tag: PortfolioTag) -> PortfolioTagResponse: + return PortfolioTagResponse(portfolio_id=tag.portfolio_id, tag=tag.tag, created_at=tag.created_at) + + +def _normalize_tag(value: str) -> str: + normalized = value.strip() + if not normalized: + raise ValueError("tag는 비어 있을 수 없습니다") + return normalized + + +def _normalize_tag_list(values: list[str] | None) -> list[str] | None: + if not values: + return None + normalized: list[str] = [] + seen: set[str] = set() + for value in values: + item = _normalize_tag(value) + if item in seen: + continue + seen.add(item) + normalized.append(item) + return normalized or None + + def _to_detail_response(status_result) -> PortfolioDetailResponse: return PortfolioDetailResponse( portfolio=_to_portfolio_summary(status_result.portfolio), @@ -144,8 +172,21 @@ class PortfolioService: broker: str | None = None, status: str | None = None, owner_user_id: str | None = None, + include_tags: list[str] | None = None, + exclude_tags: list[str] | None = None, + match_mode: str | None = None, ) -> list[PortfolioListItem]: - portfolios = self._repository.list_portfolios(broker=broker, status=status, owner_user_id=owner_user_id) + normalized_mode = None if match_mode is None else match_mode.upper() + if normalized_mode is not None and normalized_mode not in {"AND", "OR"}: + raise ValueError("matchMode는 AND 또는 OR 여야 합니다") + portfolios = self._repository.list_portfolios( + broker=broker, + status=status, + owner_user_id=owner_user_id, + include_tags=_normalize_tag_list(include_tags), + exclude_tags=_normalize_tag_list(exclude_tags), + match_mode=normalized_mode, + ) return [ PortfolioListItem( **_to_portfolio_summary(portfolio).model_dump(), @@ -302,6 +343,19 @@ class PortfolioService: self._repository.get(portfolio_id) self._repository.delete_comment(portfolio_id, comment_id) + def add_tag(self, portfolio_id: str, payload: PortfolioTagMutationRequest) -> PortfolioTagResponse: + self._repository.get(portfolio_id) + tag = self._repository.add_tag(portfolio_id, tag=_normalize_tag(payload.tag)) + return _to_tag_response(tag) + + def list_tags(self, portfolio_id: str) -> list[PortfolioTagResponse]: + self._repository.get(portfolio_id) + return [_to_tag_response(tag) for tag in self._repository.list_tags(portfolio_id)] + + def remove_tag(self, portfolio_id: str, payload: PortfolioTagMutationRequest) -> None: + self._repository.get(portfolio_id) + self._repository.remove_tag(portfolio_id, tag=_normalize_tag(payload.tag)) + def get_portfolio_service( repository: PortfolioRepository = Depends(get_portfolio_repository), diff --git a/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md b/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md index 5a58a03..7789709 100644 --- a/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md +++ b/docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md @@ -1,6 +1,6 @@ # REQ-002: 포트폴리오 태깅 및 태그 기반 검색 지원 -- Status: Proposed +- Status: Approved - Type: Interface - Priority: Medium - Owner: Justin @@ -12,27 +12,30 @@ ## Requirement - 시스템은 각 포트폴리오에 대해 하나 이상의 태그를 추가하고 제거할 수 있어야 한다. - 시스템은 HTTP API와 CLI를 통해 동일한 포트폴리오 태깅 기능을 제공해야 한다. +- 시스템은 태그 추가 시 입력 문자열의 앞뒤 공백을 제거한 값을 저장 및 비교 기준으로 사용해야 하며, 정규화 결과가 빈 문자열이면 validation 오류를 반환해야 한다. +- 시스템은 태그 문자열의 대소문자, 내부 공백, 네임스페이스 구분자(`:`)를 정규화 과정에서 변경하지 않아야 하며, 앞뒤 공백 제거 외의 추가 정규화는 수행하지 않아야 한다. - 시스템은 둘 이상의 태그를 조건으로 포트폴리오를 검색할 수 있어야 하며, 검색 요청에서 `AND` 또는 `OR` 매칭 방식을 선택할 수 있어야 한다. -- 시스템은 다운스트림 소비자가 태그 기반 목록 조회 시 사용할 수 있도록 포함 태그(`includeTags`)와 제외 태그(`excludeTags`)를 동시에 지원해야 한다. +- 시스템은 HTTP API의 포트폴리오 목록 조회 인터페이스와 CLI의 동일 의미 인터페이스에서 포함 태그(`includeTags`), 제외 태그(`excludeTags`), 포함 태그 매칭 방식(`matchMode`: `AND` 또는 `OR`)을 동시에 지원해야 한다. +- 시스템은 `matchMode`를 `includeTags` 집합에만 적용해야 하며, `excludeTags`는 요청된 태그 중 하나라도 일치하면 해당 포트폴리오를 결과에서 제외해야 한다. - 시스템은 태그 기반 조회에서 `grade:*`, `blacklist:*`와 같은 태그 네임스페이스를 그대로 보존하고 필터 조건으로 사용할 수 있어야 한다. - 시스템은 공백만으로 이루어지지 않은 태그 문자열을 추가 대상으로 받아들여야 한다. -- 시스템은 동일 포트폴리오에 이미 연결된 동일한 태그 문자열을 다시 추가하려는 요청에 대해 `409 Conflict` 또는 이에 대응하는 CLI 오류를 반환해야 하며, 기존 태그 상태를 변경하지 않아야 한다. +- 시스템은 동일 포트폴리오에 이미 연결된 태그와 앞뒤 공백 제거 후 동일한 태그 문자열을 다시 추가하려는 요청에 대해 `409 Conflict` 또는 이에 대응하는 CLI 오류를 반환해야 하며, 기존 태그 상태를 변경하지 않아야 한다. - 시스템은 태그 추가·삭제·검색 요청에 대해 기존 포트폴리오 접근 권한 정책을 적용해야 하며, 인증 실패 또는 권한 부족 시 기존 인증 오류 계약에 따라 요청을 거부해야 한다. - 시스템은 존재하지 않는 `portfolio_id`를 대상으로 한 태그 추가·삭제 요청에 대해 Not Found 오류를 반환해야 한다. - 시스템은 태그 검색 결과에서 각 포트폴리오를 중복 없이 한 번만 반환해야 한다. - 시스템은 태그 문자열 길이 또는 포트폴리오당 태그 개수에 대해 애플리케이션 정의 최대 제한만을 이유로 요청을 거부하지 않아야 한다. ## Acceptance Criteria -1. Given 기존 포트폴리오가 존재하고 권한이 있는 사용자가 API 또는 CLI로 새 태그를 추가할 때, When 공백만으로 이루어지지 않은 태그 문자열을 제출하면, Then 시스템은 해당 포트폴리오에 태그를 연결하고 후속 조회에서 그 태그가 포함된 상태를 반환해야 한다. +1. Given 기존 포트폴리오가 존재하고 권한이 있는 사용자가 API 또는 CLI로 새 태그를 추가할 때, When 공백만으로 이루어지지 않은 태그 문자열을 제출하면, Then 시스템은 입력값의 앞뒤 공백을 제거한 태그를 해당 포트폴리오에 연결하고 후속 조회에서 그 정규화된 태그가 포함된 상태를 반환해야 한다. 2. Given 특정 포트폴리오에 이미 연결된 태그가 있을 때, When 사용자가 API 또는 CLI로 해당 태그를 제거하면, Then 시스템은 성공 응답을 반환하고 후속 조회 및 태그 검색 결과에서 그 연결을 노출하지 않아야 한다. -3. Given 여러 포트폴리오에 서로 다른 태그 조합이 연결되어 있을 때, When 사용자가 API 또는 CLI로 둘 이상의 태그와 `AND` 모드를 지정해 검색하면, Then 시스템은 요청한 모든 태그를 포함한 포트폴리오만 반환해야 한다. -4. Given 여러 포트폴리오에 서로 다른 태그 조합이 연결되어 있을 때, When 사용자가 API 또는 CLI로 둘 이상의 태그와 `OR` 모드를 지정해 검색하면, Then 시스템은 요청한 태그 중 하나 이상을 포함한 포트폴리오를 반환해야 한다. -5. Given 동일 포트폴리오에 이미 동일한 태그 문자열이 존재할 때, When 사용자가 API 또는 CLI로 같은 태그를 다시 추가하면, Then 시스템은 `409 Conflict` 응답 또는 이에 대응하는 CLI 오류를 반환하고 중복 태그 연결을 생성하지 않아야 한다. +3. Given 여러 포트폴리오에 서로 다른 태그 조합이 연결되어 있을 때, When 사용자가 API 또는 CLI로 `includeTags`에 둘 이상의 태그와 `matchMode=AND`를 지정해 검색하면, Then 시스템은 요청한 모든 포함 태그를 가진 포트폴리오만 반환해야 한다. +4. Given 여러 포트폴리오에 서로 다른 태그 조합이 연결되어 있을 때, When 사용자가 API 또는 CLI로 `includeTags`에 둘 이상의 태그와 `matchMode=OR`를 지정해 검색하면, Then 시스템은 요청한 포함 태그 중 하나 이상을 가진 포트폴리오를 반환해야 한다. +5. Given 동일 포트폴리오에 이미 앞뒤 공백 제거 후 동일한 태그 문자열이 존재할 때, When 사용자가 API 또는 CLI로 같은 태그를 다시 추가하면, Then 시스템은 `409 Conflict` 응답 또는 이에 대응하는 CLI 오류를 반환하고 중복 태그 연결을 생성하지 않아야 한다. 6. Given 인증되지 않았거나 해당 포트폴리오에 접근 권한이 없는 사용자가 API 또는 CLI로 태그 추가·삭제·검색을 시도할 때, When 요청을 전송하면, Then 시스템은 기존 인증/인가 계약에 따라 `401` 또는 `403` 응답을 반환하고 태그 상태를 변경하지 않아야 한다. 7. Given 존재하지 않는 `portfolio_id`가 요청에 포함될 때, When 사용자가 API 또는 CLI로 태그 추가 또는 삭제를 시도하면, Then 시스템은 `404` 응답 또는 이에 대응하는 CLI 오류를 반환하고 어떤 태그 연결도 생성·변경하지 않아야 한다. -8. Given 포트폴리오에 이미 여러 태그가 연결되어 있고 사용자가 긴 태그 문자열을 추가할 때, When API 또는 CLI로 태그 추가를 요청하면, Then 시스템은 애플리케이션 정의 최대 길이 제한 또는 최대 개수 제한만을 이유로 요청을 거부하지 않아야 한다. +8. Given 포트폴리오에 이미 여러 태그가 연결되어 있고 사용자가 긴 태그 문자열을 추가할 때, When API 또는 CLI로 태그 추가를 요청하면, Then 시스템은 앞뒤 공백 제거 후 빈 문자열이 아닌 한 애플리케이션 정의 최대 길이 제한 또는 최대 개수 제한만을 이유로 요청을 거부하지 않아야 한다. 9. Given `OR` 모드의 다중 태그 검색에서 하나의 포트폴리오가 두 개 이상의 요청 태그와 동시에 일치할 때, When 사용자가 검색을 수행하면, Then 시스템은 해당 포트폴리오를 결과에 한 번만 반환해야 한다. -10. Given 소비 서비스가 `includeTags=["grade:A", "grade:B"]`, `excludeTags=["blacklist:trade"]` 조건으로 목록 조회를 요청한 상태, When 태그 필터 검색을 수행하면, Then 시스템은 포함 조건을 만족하고 제외 조건을 만족하지 않는 항목만 반환해야 한다. +10. Given 소비 서비스가 `includeTags=["grade:A", "grade:B"]`, `excludeTags=["blacklist:trade"]`, `matchMode=OR` 조건으로 목록 조회를 요청한 상태, When 태그 필터 검색을 수행하면, Then 시스템은 `grade:A` 또는 `grade:B` 중 하나 이상을 포함하고 `blacklist:trade`를 하나라도 포함하지 않는 항목만 반환해야 한다. ## Impacted Area - Docs: `docs/technical/api-contracts.md`, `docs/technical/persistence-and-migrations.md`, `docs/SRS/index.md`, `docs/SRS/traceability-matrix.md` @@ -43,10 +46,74 @@ - 본 requirement의 검색 범위는 다중 태그 검색을 포함하며, 호출자는 `AND` 또는 `OR` 모드를 선택할 수 있다. - 태그 최대 길이와 포트폴리오당 최대 태그 개수에 대한 애플리케이션 정의 상한은 두지 않는다. - 동일 포트폴리오에 같은 태그 문자열을 다시 추가하는 요청은 `409 Conflict` 또는 이에 대응하는 CLI 오류로 처리한다. +- 태그 정규화는 앞뒤 공백 제거만 수행하며, 대소문자와 내부 문자열은 보존한다. +- HTTP API와 CLI는 `includeTags`, `excludeTags`, `matchMode`에 대해 동일 의미의 검색 계약을 제공한다. - 특정 소비 서비스의 입력 고정/스냅샷 책임은 소비 서비스 측 requirement에서 관리하며, portfolio-hub는 태그 등록/검색 계약만 제공한다. ## Change Log +### CHG-20260414-02 +- Date: 2026-04-14 +- Author: GitHub Copilot + +#### Change Summary +- REQ-002를 Proposed에서 Approved로 전환했다. + +#### Intent +- 태그 정규화 규칙과 태그 검색 입력 계약이 승인 기준을 충족했음을 반영하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 없음 +- Updated: + - Status를 Approved로 전환 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` + - `docs/SRS/index.md` + - `docs/SRS/traceability-matrix.md` +- Modules: + - 없음 +- Tests: + - 없음 + +#### Validation Plan +- 승인 후 technical spec 동기화 시 REQ-002의 정규화 및 검색 계약이 관련 technical 문서에 동일하게 반영되는지 확인한다. + +### CHG-20260414-01 +- Date: 2026-04-14 +- Author: GitHub Copilot + +#### Change Summary +- REQ-002의 태그 정규화 규칙과 태그 검색 입력 계약을 구체화해 승인 blocker를 해소할 수 있도록 보강했다. + +#### Intent +- 태그 중복 판단과 include/exclude 검색 동작이 구현마다 다르게 해석되지 않도록 인터페이스 계약을 고정하기 위함이다. + +#### Acceptance Criteria Delta +- Added: + - 앞뒤 공백 제거 기반 태그 정규화 규칙 + - `includeTags`, `excludeTags`, `matchMode`의 결합 규칙 +- Updated: + - AC1, AC3, AC4, AC5, AC8, AC10의 입력/비교 기준 명확화 +- Removed: + - 없음 + +#### Impacted Area Delta +- Docs: + - `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` + - `docs/SRS/index.md` +- Modules: + - tag input normalization 및 검색 query parser 구현 경로 +- Tests: + - 태그 정규화, 중복 추가 충돌, include/exclude 조합 검색 검증 시나리오 + +#### Validation Plan +- 문서 리뷰 시 태그 정규화 규칙과 `includeTags`/`excludeTags`/`matchMode` 조합 규칙이 각각 독립 테스트로 매핑 가능한지 확인한다. + ### CHG-20260413-05 - Date: 2026-04-13 - Author: GitHub Copilot diff --git a/docs/SRS/index.md b/docs/SRS/index.md index bb521e7..01ff8dd 100644 --- a/docs/SRS/index.md +++ b/docs/SRS/index.md @@ -22,16 +22,16 @@ | REQ ID | Title | Type | Status | Priority | Owner | File | |---|---|---|---|---|---|---| | REQ-001 | 포트폴리오 댓글형 메모 이력 지원 | Interface | Approved | Medium | Justin | `docs/SRS/Interface/REQ-001_portfolio-comment-history.md` | -| REQ-002 | 포트폴리오 태깅 및 태그 기반 검색 지원 | Interface | Proposed | Medium | Justin | `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` | +| REQ-002 | 포트폴리오 태깅 및 태그 기반 검색 지원 | Interface | Approved | Medium | Justin | `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` | | REQ-003 | 포트폴리오 필수 필드 및 거래소/시장 데이터 정의 | Data | Proposed | Medium | Justin | `docs/SRS/Data/REQ-003_portfolio-required-fields.md` | | REQ-004 | 포트폴리오 소유주 기반 접근 제어 | Interface | Proposed | High | Justin | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | ## Recent Change Log Summary +- 2026-04-14 — `REQ-002`를 Approved로 전환했다. +- 2026-04-14 — `REQ-002`에 태그 정규화 규칙과 `includeTags`/`excludeTags`/`matchMode` 검색 계약을 추가해 승인 blocker를 줄였다. - 2026-04-14 — `REQ-004`에 사용자 귀속 봇 principal(`bot_principals.owner_user_id`) 및 봇 토큰 실효 사용자 매핑 규칙(AC6, AC7)을 추가했다. - 2026-04-13 — `REQ-004` 포트폴리오 소유주(`owner_user_id`) 및 소유권 기반 접근 제어 requirement를 추가했다. - 2026-04-13 — `REQ-003`에서 태그 필터 기반 조회의 결정성 유지 책임을 소비 서비스(stock-picker)로 이관했다. -- 2026-04-13 — `REQ-003`에서 `sourceVersion`/`sourceTimestamp` 요구를 제거하고 태그 필터 기반 조회 결정성 계약으로 정리했다. -- 2026-04-13 — `REQ-003`의 소비자 표현을 특정 서비스 전용에서 태그 기반 조회 소비 서비스 일반 계약으로 정리했다. ## Status Transition Evidence - `Proposed` → `Approved`: Owner가 지정되고 requirement 문서 리뷰가 완료되며 acceptance criteria가 누락 없이 검토되어야 한다. diff --git a/docs/SRS/traceability-matrix.md b/docs/SRS/traceability-matrix.md index b2214fb..895ec57 100644 --- a/docs/SRS/traceability-matrix.md +++ b/docs/SRS/traceability-matrix.md @@ -12,7 +12,7 @@ | REQ ID | AC Mapping | Requirement File | Implementation Issue | PR | Test Evidence | Requirement Status | Delivery Status | Last Updated | |---|---|---|---|---|---|---|---|---| | REQ-001 | AC1, AC2, AC3, AC4, AC5, AC6, AC7 | `docs/SRS/Interface/REQ-001_portfolio-comment-history.md` | #2 | TBD | TBD | Approved | In Progress | 2026-04-10 | -| REQ-002 | AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9 | `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` | TBD | TBD | TBD | Proposed | Planned | 2026-04-10 | +| REQ-002 | AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9, AC10 | `docs/SRS/Interface/REQ-002_portfolio-tagging-and-search.md` | TBD | TBD | `tests/test_portfolio_tags.py`, `tests/test_cli.py`, local `pytest` pass (65 passed, 2026-04-14) | Approved | Verified | 2026-04-14 | | REQ-003 | AC1, AC2, AC3, AC4, AC5 | `docs/SRS/Data/REQ-003_portfolio-required-fields.md` | TBD | TBD | TBD | Proposed | Planned | 2026-04-10 | | REQ-004 | AC1, AC2, AC3, AC4, AC5, AC6, AC7 | `docs/SRS/Interface/REQ-004_portfolio-ownership-access-control.md` | TBD | TBD | `tests/test_portfolio_ownership.py`, `tests/test_users.py`, `tests/test_auth.py`, `tests/test_portfolio_comments.py`, local `pytest` pass (2026-04-14) | Proposed | In Progress | 2026-04-14 | diff --git a/docs/technical/api-contracts.md b/docs/technical/api-contracts.md index 547efdd..763cfc4 100644 --- a/docs/technical/api-contracts.md +++ b/docs/technical/api-contracts.md @@ -54,6 +54,15 @@ - `POST /portfolios/{portfolio_id}/transfer` - `POST /portfolios/{portfolio_id}/reset` - `DELETE /portfolios/{portfolio_id}` +- `POST /portfolios/{portfolio_id}/tags` +- `GET /portfolios/{portfolio_id}/tags` +- `DELETE /portfolios/{portfolio_id}/tags` + +REQ-002 정렬 메모: + +- 포트폴리오 목록 조회는 반복 query parameter `includeTags`, `excludeTags`, `matchMode`를 지원한다. +- 태그 mutation은 기존 포트폴리오 리소스 하위 경로 `/portfolios/{portfolio_id}/tags`로 구현한다. +- CLI는 `portfolio tag add|list|remove` 및 `portfolio list --include-tag/--exclude-tag/--match-mode` 계약을 제공한다. ### 포트폴리오 댓글 (`REQ-001`) - `POST /portfolios/{portfolio_id}/comments` @@ -106,12 +115,17 @@ - Query: - `broker: string | null` - `status_filter: string | null` + - `includeTags: string[] | null` + - `excludeTags: string[] | null` + - `matchMode: "AND" | "OR" | null` (`includeTags`에만 적용) - 응답 `200`: `PortfolioListItem[]` +- 오류: + - `400`: `matchMode`가 `AND` 또는 `OR`가 아님 예시: ```bash -curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled" \ +curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled&includeTags=grade:A&excludeTags=blacklist:trade&matchMode=OR" \ -H "Authorization: Bearer " ``` @@ -123,6 +137,7 @@ curl -X GET "http://localhost:8000/portfolios?broker=upbit&status_filter=enabled "status": "enabled", "name": "Bitcoin", "symbol": "KRW-BTC", + "tags": ["grade:A"], "cash_balance": 1000000.0, "asset_quantity": 0.1, "price": 100000000.0, @@ -157,6 +172,7 @@ curl -X GET "http://localhost:8000/portfolios/krw-btc?recent_trades=5" \ "status": "enabled", "name": "Bitcoin", "symbol": "KRW-BTC", + "tags": ["grade:A"], "owner_user_id": "user-1", "cash_balance": 1000000.0, "asset_quantity": 0.1, @@ -246,6 +262,16 @@ curl -X PATCH "http://localhost:8000/portfolios/krw-btc" \ - `POST /portfolios/{portfolio_id}/transfer`: `TransferRequest` -> `PortfolioDetailResponse` - `POST /portfolios/{portfolio_id}/reset`: `ResetPortfolioRequest` -> `PortfolioDetailResponse` - `DELETE /portfolios/{portfolio_id}`: admin only, query `keep_ledger: bool = false`, 응답 `204` +- `POST /portfolios/{portfolio_id}/tags`: `PortfolioTagMutationRequest` -> `PortfolioTagResponse` +- `GET /portfolios/{portfolio_id}/tags`: `PortfolioTagResponse[]` +- `DELETE /portfolios/{portfolio_id}/tags`: body `PortfolioTagMutationRequest`, 응답 `204` + +태그 엔드포인트 추가 규칙: + +- `POST`/`DELETE`는 `trade-write`, `GET`은 `read` +- 비관리자 요청은 기존 ownership 검사를 동일하게 적용 +- 입력 `tag`는 앞뒤 공백을 제거한 값을 저장/비교 기준으로 사용 +- 정규화 후 빈 문자열은 `422`, 중복 태그 추가는 `409`, 포트폴리오 또는 태그 미존재는 `404` 대표 요청: @@ -277,6 +303,19 @@ curl -X POST "http://localhost:8000/portfolios/krw-btc/reset" \ curl -X DELETE "http://localhost:8000/portfolios/krw-btc?keep_ledger=false" \ -H "Authorization: Bearer " + +curl -X POST "http://localhost:8000/portfolios/krw-btc/tags" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"tag": " grade:A "}' + +curl -X GET "http://localhost:8000/portfolios/krw-btc/tags" \ + -H "Authorization: Bearer " + +curl -X DELETE "http://localhost:8000/portfolios/krw-btc/tags" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"tag": "grade:A"}' ``` #### 포트폴리오 댓글 (`REQ-001`) @@ -703,7 +742,7 @@ curl -X POST "http://localhost:8000/users" \ ## 문서 동기화 규칙 - `app/api/` 또는 `app/schemas/` 변경 시 본 문서를 같은 변경 단위에서 갱신한다. -- `REQ-002` 태그 검색 API가 구현되면 요청/응답 파라미터와 필터 의미를 본 문서에 추가한다. +- `REQ-002` 태그 검색 구현은 승인된 정규화/필터 의미와 실제 route/CLI 계약을 모두 본 문서에 유지한다. ## Open Implementation Points - 에러 응답 body를 공통 포맷(`error_code`, `message`, `details`)으로 강제할지 결정한다. diff --git a/docs/technical/index.md b/docs/technical/index.md index 759cbac..4486289 100644 --- a/docs/technical/index.md +++ b/docs/technical/index.md @@ -19,6 +19,7 @@ | `settings-and-env-loading.md` | `.env`/환경변수 로딩, 필수 키, CORS/운영 설정 규칙 | N/A | | `architecture-overview.md` | 시스템 개요, 모듈 책임, 런타임 흐름, 테스트/호환성 기준 | `REQ-003` | | `api-contracts.md` | HTTP API 상세 계약, request/response 모델, 대표 payload 예시 | `REQ-001`, `REQ-002`, `REQ-003` | +| `portfolio-tagging-and-search-design.md` | 태그 정규화, API/CLI 검색 파라미터, 모듈 책임, 구현 단계 설계 | `REQ-002` | | `persistence-and-migrations.md` | PostgreSQL/Timescale 연동, 리포지토리 스위치, Alembic 운영, 스키마 baseline | `REQ-001`, `REQ-003` | | `auth-and-token-lifecycle.md` | 로그인/토큰/봇 주체 라이프사이클 및 권한 모델 | N/A | | `portfolio-ownership-and-auth-design.md` | 포트폴리오 소유주(`owner_user_id`) 도입, 소유권 접근 제어, 검증 시나리오 설계 | `REQ-004` | @@ -30,7 +31,7 @@ | REQ ID | Technical Docs | | --- | --- | | `REQ-001` | `api-contracts.md`, `persistence-and-migrations.md` | -| `REQ-002` | `api-contracts.md` | +| `REQ-002` | `api-contracts.md`, `portfolio-tagging-and-search-design.md`, `persistence-and-migrations.md` | | `REQ-003` | `architecture-overview.md`, `api-contracts.md`, `persistence-and-migrations.md` | | `REQ-004` | `auth-and-token-lifecycle.md`, `api-contracts.md`, `persistence-and-migrations.md`, `portfolio-ownership-and-auth-design.md` | diff --git a/docs/technical/persistence-and-migrations.md b/docs/technical/persistence-and-migrations.md index ffb0723..dfd77b6 100644 --- a/docs/technical/persistence-and-migrations.md +++ b/docs/technical/persistence-and-migrations.md @@ -1,6 +1,6 @@ # Persistence and Migrations -- Related REQ: `REQ-001`, `REQ-003`, `REQ-004` +- Related REQ: `REQ-001`, `REQ-002`, `REQ-003`, `REQ-004` ## 목표 DB 기반 영속성 경로를 안정적으로 운영하고, 스키마 변경 이력과 문서 동기화를 일관되게 유지한다. @@ -257,6 +257,48 @@ Conditional migration behavior: - application layer cascade: - 포트폴리오 삭제 시 연관 `portfolio_comments`를 hard delete 한다. +## REQ-002 Tagging Storage Design + +목표: + +- 포트폴리오별 태그 연결을 중복 없이 저장한다. +- 앞뒤 공백 제거 기반 정규화와 검색용 include/exclude 조건을 DB 계층에서 일관되게 지원한다. + +구현 저장 모델: + +- 신규 테이블 `portfolio_tags` + - 복합 PK(`portfolio_id`, `tag_value`) 사용 + - `portfolio_id`: 대상 포트폴리오 + - `tag_value`: 앞뒤 공백 제거 후 저장되는 태그 값 + - `created_at`: 연결 생성 시각 +- 동일 포트폴리오 내 중복 태그는 PK 자체로 방지한다. + +정규화 책임: + +- API/CLI 입력 계층에서 앞뒤 공백 제거를 수행한다. +- repository는 이미 정규화된 `tag_value`를 기준으로 insert/delete/query를 수행한다. +- DB는 `UNIQUE (portfolio_id, tag_value)`로 중복 insert를 방지하고 application은 이를 `409 Conflict`로 매핑한다. + +검색 전략: + +- `includeTags`와 `excludeTags`는 모두 정규화된 태그 값 기준으로 비교한다. +- `matchMode=AND`는 `GROUP BY portfolio_id` + `HAVING COUNT(DISTINCT tag_value) = ` 전략을 기본안으로 둔다. +- `matchMode=OR`는 `WHERE tag_value IN (...)` + distinct portfolio projection을 기본안으로 둔다. +- `excludeTags`는 anti-join 또는 `NOT EXISTS` 전략을 사용한다. + +권장 인덱스: + +- `UNIQUE (portfolio_id, tag_value)` +- `INDEX (tag_value, portfolio_id)` +- owner-scoped 목록 필터와 결합될 수 있으므로 `portfolios.owner_user_id` 인덱스와 함께 실행 계획을 검토한다. + +적용 마이그레이션 단계: + +1. `20260414_0012_portfolio_tags.py`에서 `portfolio_tags` 테이블 생성 +2. `fk_portfolio_tags_portfolio` FK와 `idx_portfolio_tags_tag_portfolio` 인덱스 생성 +3. API/CLI/repository가 동일 revision 기준으로 태그 mutation 및 검색을 사용 +4. 필요 시 exact-match 중심 추가 인덱스를 검토 + ## Validation Notes - 마이그레이션은 빈 DB와 기존 DB 모두에서 연속 적용 가능해야 한다. - 파괴적 변경(drop/rename)은 백필 또는 호환 단계 계획 없이 직접 반영하지 않는다. @@ -276,7 +318,9 @@ Conditional migration behavior: | 20260403_0008 | migrations/versions/20260403_0008_portfolio_name_column.py | Add non-null `portfolios.name` and backfill from `symbol` | | 20260410_0009 | migrations/versions/20260410_0009_portfolio_comments_table.py | Add `portfolio_comments` table for per-portfolio memo history | | 20260413_0010 | migrations/versions/20260413_0010_portfolio_owner_user_id.py | Add `portfolios.owner_user_id`, backfill owner, create owner index, and add owner FK | +| 20260414_0011 | migrations/versions/20260414_0011_bot_principal_owner_user_id.py | Restore `bot_principals.owner_user_id`, backfill owner, create owner index, and add owner FK | +| 20260414_0012 | migrations/versions/20260414_0012_portfolio_tags.py | Add `portfolio_tags`, portfolio FK, and tag lookup index for REQ-002 | ## Open Implementation Points - `portfolio_ledger`의 Timescale hypertable 강제 여부와 보존 정책(retention)을 확정한다. -- `REQ-002` 태그 검색 구현 시 인덱스 전략(B-tree/GiN 등)과 마이그레이션 단계를 추가 정의한다. +- owner-scoped 조회와 태그 필터를 함께 사용할 때 결과 집합이 ownership 경계를 넘지 않아야 한다. diff --git a/docs/technical/portfolio-tagging-and-search-design.md b/docs/technical/portfolio-tagging-and-search-design.md new file mode 100644 index 0000000..bc3a3fb --- /dev/null +++ b/docs/technical/portfolio-tagging-and-search-design.md @@ -0,0 +1,109 @@ +# Portfolio Tagging and Search Design + +- Related REQ: `REQ-002` + +## 목표 +- 승인된 `REQ-002`를 구현 가능한 내부 설계로 정리한다. +- 태그 정규화, 검색 파라미터 의미, 모듈 책임, 저장소/마이그레이션 방향을 한 문서에서 추적한다. + +## 구현 범위 +- 포트폴리오별 태그 추가/삭제 흐름 +- HTTP API와 CLI의 공통 태그 검색 파라미터 의미 +- repository query 전략과 DB 인덱스 후보 + +제외: + +- 태그 추천, 자동 분류, 네임스페이스 사전 관리 +- 부분 일치/전방 일치/full-text 검색 + +## 정규화 정책 +- 입력 태그는 API/CLI 계층에서 앞뒤 공백을 제거한다. +- 정규화 후 빈 문자열이면 validation 오류로 처리한다. +- 대소문자, 내부 공백, `:` 네임스페이스는 변경하지 않는다. +- 중복 판단은 정규화된 값 기준으로 수행한다. + +예시: + +- `" grade:A "` -> `"grade:A"` +- `"Grade:A"`와 `"grade:A"`는 서로 다른 태그로 취급한다. +- `"blacklist: trade"`의 내부 공백은 보존한다. + +## 모듈 책임 + +API: + +- 대상 파일: `app/api/portfolios.py` +- 책임: + - `POST/GET/DELETE /portfolios/{portfolio_id}/tags` 제공 + - `GET /portfolios`의 반복 query 파라미터 `includeTags`, `excludeTags`, `matchMode` 수신 + - 기존 ownership/auth 정책 재사용 + +CLI: + +- 대상 파일: `app/cli/commands/portfolio.py` +- 책임: + - `portfolio tag add|list|remove` 명령 노출 + - `portfolio list --include-tag --exclude-tag --match-mode` 옵션 제공 + +Schema: + +- 대상 파일: `app/schemas/portfolio.py` +- 책임: + - tag mutation request/response 모델 정의 + - list query 파라미터 contract 정리 + +Service: + +- 대상 파일: `app/services/portfolio_service.py` +- 책임: + - 정규화된 태그 입력 검증 + - repository 호출 orchestration + - duplicate/not-found/auth 오류를 API/CLI가 소비할 수 있는 에러로 유지 + +Repository: + +- 대상 파일: `app/repositories/portfolio_repository.py`, `app/repositories/postgres.py` +- 책임: + - 태그 연결 추가/삭제 + - include/exclude + matchMode query 제공 + - duplicate insert를 명시적 오류로 변환 + +## 검색 파라미터 의미 + +- `includeTags` + - 포함해야 하는 정규화 태그 목록 +- `excludeTags` + - 하나라도 일치하면 제외할 정규화 태그 목록 +- `matchMode` + - `includeTags`에만 적용 + - `AND`: 모든 include tag 필요 + - `OR`: 하나 이상의 include tag 필요 + +처리 순서: + +1. 입력 태그 정규화 +2. ownership/auth 필터 적용 +3. include 조건 계산 +4. exclude 조건 제거 +5. 중복 portfolio 제거 +6. 기존 recency ordering과 충돌 여부 확인 + +## Validation Notes +- duplicate tag add는 `UNIQUE (portfolio_id, tag_value)` 또는 동등한 repository 검증으로 `409`에 매핑해야 한다. +- 존재하지 않는 포트폴리오에 대한 add/remove는 `404`로 매핑해야 한다. +- 검색 결과는 `OR` 조건에서 중복 없이 한 번만 반환해야 한다. +- owner-scoped 조회와 태그 필터를 함께 사용할 때 결과 집합이 ownership 경계를 넘지 않아야 한다. + +## 파일 위치 및 구현 후보 +- API route extension: `app/api/portfolios.py` +- CLI extension: `app/cli/commands/portfolio.py` +- schema extension: `app/schemas/portfolio.py` +- service orchestration: `app/services/portfolio_service.py` +- repository contract: `app/repositories/portfolio_repository.py` +- Postgres query implementation: `app/repositories/postgres.py` +- migration: `migrations/versions/20260414_0012_portfolio_tags.py` + +## 구현 결정 +- 태그 mutation은 기존 포트폴리오 리소스 하위 경로 `/portfolios/{portfolio_id}/tags`로 고정했다. +- `GET /portfolios` 태그 필터는 반복 query parameter 방식으로 구현했다. +- CLI 다중 태그 입력은 반복 옵션 `--include-tag`, `--exclude-tag`로 구현했다. \ No newline at end of file diff --git a/migrations/versions/20260414_0012_portfolio_tags.py b/migrations/versions/20260414_0012_portfolio_tags.py new file mode 100644 index 0000000..8b3bd6d --- /dev/null +++ b/migrations/versions/20260414_0012_portfolio_tags.py @@ -0,0 +1,79 @@ +"""add portfolio tags table + +Revision ID: 20260414_0012 +Revises: 20260414_0011 +Create Date: 2026-04-14 16:10:00 +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +from sqlalchemy import inspect + + +revision = "20260414_0012" +down_revision = "20260414_0011" +branch_labels = None +depends_on = None + + +def _table_exists(inspector, table_name: str) -> bool: + return table_name in inspector.get_table_names() + + +def _index_exists(inspector, table_name: str, index_name: str) -> bool: + return any(index["name"] == index_name for index in inspector.get_indexes(table_name)) + + +def _fk_exists(inspector, table_name: str, fk_name: str) -> bool: + return any(fk.get("name") == fk_name for fk in inspector.get_foreign_keys(table_name)) + + +def upgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + + if not _table_exists(inspector, "portfolio_tags"): + op.create_table( + "portfolio_tags", + sa.Column("portfolio_id", sa.Text(), nullable=False), + sa.Column("tag_value", sa.Text(), nullable=False), + sa.Column("created_at", sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.text("NOW()")), + sa.PrimaryKeyConstraint("portfolio_id", "tag_value", name="pk_portfolio_tags"), + ) + + inspector = inspect(bind) + if not _fk_exists(inspector, "portfolio_tags", "fk_portfolio_tags_portfolio"): + op.create_foreign_key( + "fk_portfolio_tags_portfolio", + "portfolio_tags", + "portfolios", + ["portfolio_id"], + ["portfolio_id"], + ondelete="CASCADE", + ) + + inspector = inspect(bind) + if not _index_exists(inspector, "portfolio_tags", "idx_portfolio_tags_tag_portfolio"): + op.create_index( + "idx_portfolio_tags_tag_portfolio", + "portfolio_tags", + ["tag_value", "portfolio_id"], + unique=False, + ) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + + if _table_exists(inspector, "portfolio_tags"): + if _fk_exists(inspector, "portfolio_tags", "fk_portfolio_tags_portfolio"): + op.drop_constraint("fk_portfolio_tags_portfolio", "portfolio_tags", type_="foreignkey") + + inspector = inspect(bind) + if _index_exists(inspector, "portfolio_tags", "idx_portfolio_tags_tag_portfolio"): + op.drop_index("idx_portfolio_tags_tag_portfolio", table_name="portfolio_tags") + + op.drop_table("portfolio_tags") \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 568f40c..f59124c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,7 +9,7 @@ from typer.testing import CliRunner from app.cli.main import create_app from app.core.config import Settings -from app.domain.models import LedgerEntry, Portfolio, PortfolioComment, StrategySet, User +from app.domain.models import LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, User class FakeRepo: @@ -21,6 +21,7 @@ class FakeRepo: self._users: dict[str, User] = {} self._user_seq = 1 self._comments: dict[str, PortfolioComment] = {} + self._tags: dict[str, PortfolioTag] = {} self.closed = False def close(self) -> None: @@ -179,6 +180,9 @@ class FakeRepo: broker: str | None = None, status: str | None = None, owner_user_id: str | None = None, + include_tags: list[str] | None = None, + exclude_tags: list[str] | None = None, + match_mode: str | None = None, ) -> list[Portfolio]: if self._portfolio is None: return [] @@ -188,8 +192,46 @@ class FakeRepo: return [] if owner_user_id is not None and self._portfolio.owner_user_id != owner_user_id: return [] + tags = set(self._portfolio.tags or []) + if include_tags: + required = set(include_tags) + mode = (match_mode or "OR").upper() + if mode == "AND" and not required.issubset(tags): + return [] + if mode != "AND" and tags.isdisjoint(required): + return [] + if exclude_tags and tags.intersection(exclude_tags): + return [] return [self._portfolio] + def add_tag(self, portfolio_id: str, *, tag: str) -> PortfolioTag: + portfolio = self.get(portfolio_id) + normalized = tag.strip() + key = f"{portfolio_id}:{normalized}" + if key in self._tags: + raise ValueError(f"이미 연결된 태그입니다: {normalized}") + created = datetime.now(timezone.utc) + item = PortfolioTag(portfolio_id=portfolio_id, tag=normalized, created_at=created) + self._tags[key] = item + portfolio.tags = sorted([*list(portfolio.tags or []), normalized]) + return item + + def list_tags(self, portfolio_id: str) -> list[PortfolioTag]: + self.get(portfolio_id) + return sorted( + [tag for tag in self._tags.values() if tag.portfolio_id == portfolio_id], + key=lambda item: item.tag, + ) + + def remove_tag(self, portfolio_id: str, *, tag: str) -> None: + portfolio = self.get(portfolio_id) + normalized = tag.strip() + key = f"{portfolio_id}:{normalized}" + if key not in self._tags: + raise ValueError("태그를 찾을 수 없습니다") + del self._tags[key] + portfolio.tags = [item for item in (portfolio.tags or []) if item != normalized] + def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: self.strategy_set = StrategySet(id="set-1", name=name, config=config, nick=f"{name}:99914b932bd3") self._strategy_sets[self.strategy_set.id] = self.strategy_set @@ -691,3 +733,39 @@ def test_cli_comment_list_nonexistent_portfolio() -> None: result = runner.invoke(runner.app, ["portfolio", "comment", "list", "nonexistent:id"]) assert result.exit_code == 1 + + +def test_cli_tag_add_and_list() -> None: + repo = FakeRepo(_portfolio()) + runner = build_runner(repo) + + add_result = runner.invoke(runner.app, ["portfolio", "tag", "add", "005930:tv", "--tag", " grade:A "]) + list_result = runner.invoke(runner.app, ["--format", "json", "portfolio", "tag", "list", "005930:tv"]) + + assert add_result.exit_code == 0 + assert "grade:A" in add_result.stdout + assert list_result.exit_code == 0 + payload = json.loads(list_result.stdout) + assert payload[0]["tag"] == "grade:A" + + +def test_cli_list_supports_tag_filters() -> None: + repo = FakeRepo(_portfolio()) + repo.add_tag("005930:tv", tag="grade:A") + runner = build_runner(repo) + + result = runner.invoke(runner.app, ["portfolio", "list", "--include-tag", "grade:A", "--match-mode", "AND"]) + + assert result.exit_code == 0 + assert "005930:tv" in result.stdout + + +def test_cli_tag_remove_flow() -> None: + repo = FakeRepo(_portfolio()) + repo.add_tag("005930:tv", tag="grade:A") + runner = build_runner(repo) + + result = runner.invoke(runner.app, ["portfolio", "tag", "remove", "005930:tv", "--tag", "grade:A", "--force"]) + + assert result.exit_code == 0 + assert "Removed tag: grade:A" in result.stdout diff --git a/tests/test_domain_services.py b/tests/test_domain_services.py index 480869f..e7c44e0 100644 --- a/tests/test_domain_services.py +++ b/tests/test_domain_services.py @@ -63,6 +63,9 @@ class FakeRepo: broker: str | None = None, status: str | None = None, owner_user_id: str | None = None, + include_tags: list[str] | None = None, + exclude_tags: list[str] | None = None, + match_mode: str | None = None, ) -> list[Portfolio]: if self._portfolio is None: return [] @@ -70,6 +73,15 @@ class FakeRepo: return [] return [self._portfolio] + def add_tag(self, portfolio_id: str, *, tag: str): + raise NotImplementedError() + + def list_tags(self, portfolio_id: str): + return [] + + def remove_tag(self, portfolio_id: str, *, tag: str) -> None: + raise NotImplementedError() + def get_strategy_set(self, id: str) -> StrategySet | None: return self.strategy_set if id == self.strategy_set.id else None diff --git a/tests/test_portfolio_comments.py b/tests/test_portfolio_comments.py index be90297..7d665bd 100644 --- a/tests/test_portfolio_comments.py +++ b/tests/test_portfolio_comments.py @@ -50,11 +50,20 @@ class FakeRepo: setattr(portfolio, key, value) return portfolio - def list_portfolios(self, *, broker=None, status=None, owner_user_id=None): + def list_portfolios(self, *, broker=None, status=None, owner_user_id=None, include_tags=None, exclude_tags=None, match_mode=None): if owner_user_id is not None and self._portfolio and self._portfolio.owner_user_id != owner_user_id: return [] return [self._portfolio] if self._portfolio else [] + def add_tag(self, portfolio_id: str, *, tag: str): + raise NotImplementedError() + + def list_tags(self, portfolio_id: str): + return [] + + def remove_tag(self, portfolio_id: str, *, tag: str) -> None: + raise NotImplementedError() + def get_or_create_strategy_set(self, name: str, config: str) -> StrategySet: return self.strategy_set diff --git a/tests/test_portfolio_ownership.py b/tests/test_portfolio_ownership.py index f346a1d..c47c299 100644 --- a/tests/test_portfolio_ownership.py +++ b/tests/test_portfolio_ownership.py @@ -35,6 +35,29 @@ class FakeRepo: def list_recent_trades(self, portfolio_id: str, limit: int = 5) -> list[LedgerEntry]: return [] + def list_portfolios( + self, + *, + broker: str | None = None, + status: str | None = None, + owner_user_id: str | None = None, + include_tags: list[str] | None = None, + exclude_tags: list[str] | None = None, + match_mode: str | None = None, + ): + if owner_user_id is not None and self._portfolio.owner_user_id != owner_user_id: + return [] + return [self._portfolio] + + def add_tag(self, portfolio_id: str, *, tag: str): + raise NotImplementedError() + + def list_tags(self, portfolio_id: str): + return [] + + def remove_tag(self, portfolio_id: str, *, tag: str) -> None: + raise NotImplementedError() + def get_strategy_set(self, id: str): if id == self._strategy.id: return self._strategy diff --git a/tests/test_portfolio_tags.py b/tests/test_portfolio_tags.py new file mode 100644 index 0000000..7445cd0 --- /dev/null +++ b/tests/test_portfolio_tags.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +from fastapi.testclient import TestClient + +import app.api.portfolios as portfolios_api +from app.core.config import get_settings +from app.domain.models import LedgerEntry, Portfolio, PortfolioTag, StrategySet +from app.main import app + + +class FakeRepo: + def __init__(self, portfolio: Portfolio | None = None) -> None: + self._portfolio = portfolio + self._strategy = StrategySet(id="set-1", name="target_value", config="{}", nick="target_value:demo") + self._tags: dict[str, PortfolioTag] = {} + + def close(self) -> None: + return None + + def list_users(self): + return [] + + def get(self, portfolio_id: str) -> Portfolio: + if self._portfolio is None or self._portfolio.portfolio_id != portfolio_id: + raise ValueError("포트폴리오를 찾을 수 없습니다") + return self._portfolio + + def list_recent_trades(self, portfolio_id: str, limit: int = 5) -> list[LedgerEntry]: + return [] + + def get_strategy_set(self, id: str): + return self._strategy if id == self._strategy.id else None + + def list_portfolios( + self, + *, + broker: str | None = None, + status: str | None = None, + owner_user_id: str | None = None, + include_tags: list[str] | None = None, + exclude_tags: list[str] | None = None, + match_mode: str | None = None, + ) -> list[Portfolio]: + if self._portfolio is None: + return [] + if owner_user_id is not None and self._portfolio.owner_user_id != owner_user_id: + return [] + tags = set(self._portfolio.tags or []) + if include_tags: + mode = (match_mode or "OR").upper() + required = set(include_tags) + if mode == "AND" and not required.issubset(tags): + return [] + if mode != "AND" and tags.isdisjoint(required): + return [] + if exclude_tags and tags.intersection(exclude_tags): + return [] + return [self._portfolio] + + def add_tag(self, portfolio_id: str, *, tag: str) -> PortfolioTag: + portfolio = self.get(portfolio_id) + key = f"{portfolio_id}:{tag}" + if key in self._tags: + raise ValueError(f"이미 연결된 태그입니다: {tag}") + item = PortfolioTag(portfolio_id=portfolio_id, tag=tag, created_at=datetime.now(timezone.utc)) + self._tags[key] = item + portfolio.tags = sorted([*list(portfolio.tags or []), tag]) + return item + + def list_tags(self, portfolio_id: str) -> list[PortfolioTag]: + self.get(portfolio_id) + return sorted([tag for tag in self._tags.values() if tag.portfolio_id == portfolio_id], key=lambda item: item.tag) + + def remove_tag(self, portfolio_id: str, *, tag: str) -> None: + portfolio = self.get(portfolio_id) + key = f"{portfolio_id}:{tag}" + if key not in self._tags: + raise ValueError("태그를 찾을 수 없습니다") + del self._tags[key] + portfolio.tags = [item for item in (portfolio.tags or []) if item != tag] + + +def _portfolio() -> Portfolio: + return Portfolio( + portfolio_id="p1", + broker="kis", + status="enabled", + name="Samsung Electronics", + symbol="005930", + tags=[], + owner_user_id="owner-1", + cash_balance=1_000_000.0, + asset_quantity=1.0, + price=70_000.0, + profit_buffer=0.0, + active_strategy_id="set-1", + avabile_strategies=["set-1"], + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + +def _setup_client(monkeypatch, repo: FakeRepo) -> TestClient: + monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "false") + monkeypatch.setenv("PORTFOLIOHUB_READ_TOKENS", "read-token") + monkeypatch.setenv("PORTFOLIOHUB_TRADE_WRITE_TOKENS", "write-token") + monkeypatch.setenv("PORTFOLIOHUB_ADMIN_TOKENS", "admin-token") + get_settings.cache_clear() + app.dependency_overrides.clear() + monkeypatch.setattr(portfolios_api, "get_portfolio_repository_from_settings", lambda settings: repo) + return TestClient(app) + + +def test_add_tag_normalizes_and_lists(monkeypatch) -> None: + repo = FakeRepo(_portfolio()) + client = _setup_client(monkeypatch, repo) + + add_response = client.post( + "/portfolios/p1/tags", + headers={"Authorization": "Bearer write-token"}, + json={"tag": " grade:A "}, + ) + list_response = client.get("/portfolios/p1/tags", headers={"Authorization": "Bearer read-token"}) + + app.dependency_overrides.clear() + assert add_response.status_code == 201 + assert add_response.json()["tag"] == "grade:A" + assert list_response.status_code == 200 + assert list_response.json()[0]["tag"] == "grade:A" + + +def test_duplicate_tag_returns_409(monkeypatch) -> None: + repo = FakeRepo(_portfolio()) + repo.add_tag("p1", tag="grade:A") + client = _setup_client(monkeypatch, repo) + + response = client.post( + "/portfolios/p1/tags", + headers={"Authorization": "Bearer write-token"}, + json={"tag": " grade:A "}, + ) + + app.dependency_overrides.clear() + assert response.status_code == 409 + + +def test_portfolio_list_filters_by_include_and_exclude_tags(monkeypatch) -> None: + repo = FakeRepo(_portfolio()) + repo.add_tag("p1", tag="grade:A") + client = _setup_client(monkeypatch, repo) + + include_response = client.get( + "/portfolios", + headers={"Authorization": "Bearer read-token"}, + params=[("includeTags", "grade:A"), ("matchMode", "AND")], + ) + exclude_response = client.get( + "/portfolios", + headers={"Authorization": "Bearer read-token"}, + params=[("excludeTags", "grade:A")], + ) + + app.dependency_overrides.clear() + assert include_response.status_code == 200 + assert include_response.json()[0]["tags"] == ["grade:A"] + assert exclude_response.status_code == 200 + assert exclude_response.json() == [] + + +def test_remove_tag_returns_204(monkeypatch) -> None: + repo = FakeRepo(_portfolio()) + repo.add_tag("p1", tag="grade:A") + client = _setup_client(monkeypatch, repo) + + response = client.request( + "DELETE", + "/portfolios/p1/tags", + headers={"Authorization": "Bearer write-token"}, + json={"tag": "grade:A"}, + ) + + app.dependency_overrides.clear() + assert response.status_code == 204 + assert repo._portfolio is not None + assert repo._portfolio.tags == [] \ No newline at end of file From 974c3d0dbd16a9865003b453a7ccb75f36719130 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Tue, 14 Apr 2026 20:26:46 +0900 Subject: [PATCH 08/21] first commit --- .github/AGENTS.md | 23 ++ .github/SDLC-CHANGELOG.md | 315 ++++++++++++++++++ .github/START-HERE.md | 53 +++ .github/agents/builder.agent.md | 46 +++ .github/agents/planner.agent.md | 46 +++ .github/agents/releaser.agent.md | 46 +++ .github/agents/reviewer.agent.md | 46 +++ .github/copilot-instructions.md | 43 +++ .../instructions/01-discovery.instructions.md | 108 ++++++ .../instructions/02-planning.instructions.md | 114 +++++++ .../instructions/03-design.instructions.md | 49 +++ .../04-implementation.instructions.md | 49 +++ .../05-verification.instructions.md | 117 +++++++ .../instructions/06-release.instructions.md | 72 ++++ .../07-operations.instructions.md | 78 +++++ .github/prompts/design-confirm.prompt.md | 63 ++++ .github/prompts/design-draft.prompt.md | 85 +++++ .github/prompts/design-review.prompt.md | 80 +++++ .github/prompts/discovery-confirm.prompt.md | 55 +++ .github/prompts/discovery-draft.prompt.md | 107 ++++++ .github/prompts/discovery-review.prompt.md | 80 +++++ .../prompts/implementation-confirm.prompt.md | 64 ++++ .../prompts/implementation-draft.prompt.md | 85 +++++ .github/prompts/implementation.prompt.md | 75 +++++ .github/prompts/operation-confirm.prompt.md | 67 ++++ .github/prompts/operation-draft.prompt.md | 84 +++++ .github/prompts/operation-review.prompt.md | 63 ++++ .github/prompts/planning-confirm.prompt.md | 56 ++++ .github/prompts/planning-draft.prompt.md | 95 ++++++ .github/prompts/planning-review.prompt.md | 79 +++++ .github/prompts/release-confirm.prompt.md | 63 ++++ .github/prompts/release-draft.prompt.md | 85 +++++ .github/prompts/release-review.prompt.md | 80 +++++ .../prompts/verification-confirm.prompt.md | 63 ++++ .github/prompts/verification-draft.prompt.md | 84 +++++ .github/prompts/verification-review.prompt.md | 83 +++++ .../runbooks/services/api.rollback-runbook.md | 60 ++++ .../runbooks/services/web.rollback-runbook.md | 58 ++++ .../services/worker.rollback-runbook.md | 58 ++++ .github/scripts/bootstrap.sh | 102 ++++++ .github/skills/release-notes/SKILL.md | 26 ++ .../skills/requirements-refinement/SKILL.md | 26 ++ .github/skills/test-design/SKILL.md | 26 ++ .github/templates/design-note.template.md | 71 ++++ .github/templates/discovery-note.template.md | 90 +++++ .../templates/implementation-note.template.md | 62 ++++ .../planning-backlog-scores.template.csv | 4 + .github/templates/planning-note.template.md | 63 ++++ .../planning-scoring-sheet.template.csv | 4 + .github/templates/release-note.template.md | 105 ++++++ .../templates/rollback-runbook.template.md | 62 ++++ .../templates/verification-note.template.md | 83 +++++ .github/tools/calc_priority_scores.sh | 56 ++++ bootstrap/bootstrap.sh | 106 ++++++ bootstrap/install.sh | 282 ++++++++++++++++ bootstrap/source-allowlist.txt | 14 + 56 files changed, 4159 insertions(+) create mode 100644 .github/AGENTS.md create mode 100644 .github/SDLC-CHANGELOG.md create mode 100644 .github/START-HERE.md create mode 100644 .github/agents/builder.agent.md create mode 100644 .github/agents/planner.agent.md create mode 100644 .github/agents/releaser.agent.md create mode 100644 .github/agents/reviewer.agent.md create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/01-discovery.instructions.md create mode 100644 .github/instructions/02-planning.instructions.md create mode 100644 .github/instructions/03-design.instructions.md create mode 100644 .github/instructions/04-implementation.instructions.md create mode 100644 .github/instructions/05-verification.instructions.md create mode 100644 .github/instructions/06-release.instructions.md create mode 100644 .github/instructions/07-operations.instructions.md create mode 100644 .github/prompts/design-confirm.prompt.md create mode 100644 .github/prompts/design-draft.prompt.md create mode 100644 .github/prompts/design-review.prompt.md create mode 100644 .github/prompts/discovery-confirm.prompt.md create mode 100644 .github/prompts/discovery-draft.prompt.md create mode 100644 .github/prompts/discovery-review.prompt.md create mode 100644 .github/prompts/implementation-confirm.prompt.md create mode 100644 .github/prompts/implementation-draft.prompt.md create mode 100644 .github/prompts/implementation.prompt.md create mode 100644 .github/prompts/operation-confirm.prompt.md create mode 100644 .github/prompts/operation-draft.prompt.md create mode 100644 .github/prompts/operation-review.prompt.md create mode 100644 .github/prompts/planning-confirm.prompt.md create mode 100644 .github/prompts/planning-draft.prompt.md create mode 100644 .github/prompts/planning-review.prompt.md create mode 100644 .github/prompts/release-confirm.prompt.md create mode 100644 .github/prompts/release-draft.prompt.md create mode 100644 .github/prompts/release-review.prompt.md create mode 100644 .github/prompts/verification-confirm.prompt.md create mode 100644 .github/prompts/verification-draft.prompt.md create mode 100644 .github/prompts/verification-review.prompt.md create mode 100644 .github/runbooks/services/api.rollback-runbook.md create mode 100644 .github/runbooks/services/web.rollback-runbook.md create mode 100644 .github/runbooks/services/worker.rollback-runbook.md create mode 100644 .github/scripts/bootstrap.sh create mode 100644 .github/skills/release-notes/SKILL.md create mode 100644 .github/skills/requirements-refinement/SKILL.md create mode 100644 .github/skills/test-design/SKILL.md create mode 100644 .github/templates/design-note.template.md create mode 100644 .github/templates/discovery-note.template.md create mode 100644 .github/templates/implementation-note.template.md create mode 100644 .github/templates/planning-backlog-scores.template.csv create mode 100644 .github/templates/planning-note.template.md create mode 100644 .github/templates/planning-scoring-sheet.template.csv create mode 100644 .github/templates/release-note.template.md create mode 100644 .github/templates/rollback-runbook.template.md create mode 100644 .github/templates/verification-note.template.md create mode 100755 .github/tools/calc_priority_scores.sh create mode 100644 bootstrap/bootstrap.sh create mode 100644 bootstrap/install.sh create mode 100644 bootstrap/source-allowlist.txt diff --git a/.github/AGENTS.md b/.github/AGENTS.md new file mode 100644 index 0000000..5cdd60d --- /dev/null +++ b/.github/AGENTS.md @@ -0,0 +1,23 @@ +# SDLC Role Agents + +이 프로젝트는 단계 책임을 명확히 하기 위해 역할별 에이전트를 사용한다. + +## Roles + +1. Planner: Discovery, Planning, Design 중심 +2. Builder: Implementation 중심 +3. Reviewer: Verification 중심 +4. Releaser: Release, Operations 중심 + +## Handoff Rules + +1. Planner -> Builder: 우선순위, 수용 기준, 설계 근거를 전달한다. +2. Builder -> Reviewer: 변경 요약, 테스트 결과, 알려진 리스크를 전달한다. +3. Reviewer -> Releaser: 품질 상태와 배포 가능 여부를 전달한다. +4. Releaser -> Planner: 운영 피드백과 개선 항목을 다음 사이클로 전달한다. + +## Gate Rules + +1. 각 역할은 자신의 단계 게이트를 통과시키기 전 다음 역할로 넘길 수 없다. +2. Blocker 이슈는 우회가 아닌 해결 또는 승인된 예외로만 처리한다. +3. 긴급 핫픽스 후에는 반드시 Planner 단계로 환류해 누락 문서를 보강한다. diff --git a/.github/SDLC-CHANGELOG.md b/.github/SDLC-CHANGELOG.md new file mode 100644 index 0000000..6b42c07 --- /dev/null +++ b/.github/SDLC-CHANGELOG.md @@ -0,0 +1,315 @@ +# SDLC Rules Changelog + +## Update 01 - Initial Baseline (2026-04-14) + +### Scope + +1. 전역 SDLC 거버넌스 규칙 생성 +2. 7단계 instructions 생성 +3. 역할별 agents 생성 +4. 공통 skills 3종 생성 + +### Added Files + +1. .github/copilot-instructions.md +2. .github/AGENTS.md +3. .github/instructions/01-discovery.instructions.md +4. .github/instructions/02-planning.instructions.md +5. .github/instructions/03-design.instructions.md +6. .github/instructions/04-implementation.instructions.md +7. .github/instructions/05-verification.instructions.md +8. .github/instructions/06-release.instructions.md +9. .github/instructions/07-operations.instructions.md +10. .github/agents/planner.agent.md +11. .github/agents/builder.agent.md +12. .github/agents/reviewer.agent.md +13. .github/agents/releaser.agent.md +14. .github/skills/requirements-refinement/SKILL.md +15. .github/skills/test-design/SKILL.md +16. .github/skills/release-notes/SKILL.md + +### Notes + +1. 단계 스킵 금지 및 핫픽스 예외 경로를 전역 규칙에 포함 +2. 각 단계에 DoR/DoD를 명시해 게이트 기반 전환 지원 +3. 스킬 description에 Use when 트리거를 넣어 자동 탐지성 강화 + +## Update 02 - Stage Rule Hardening (2026-04-14) + +### Scope + +1. Discovery 단계 질문 템플릿 강화 +2. Planning 단계 WSJF/RICE 우선순위 규칙 추가 +3. Verification 단계 품질 게이트 엄격화 +4. Release/Operations 롤백 및 포스트모템 체크리스트 상세화 + +### Changed Files + +1. .github/instructions/01-discovery.instructions.md +2. .github/instructions/02-planning.instructions.md +3. .github/instructions/05-verification.instructions.md +4. .github/instructions/06-release.instructions.md +5. .github/instructions/07-operations.instructions.md + +### Verification + +1. 단계별 필수 섹션(템플릿/공식/게이트/체크리스트) 존재 여부 확인 +2. DoD 강화 항목 반영 여부 확인 + +### Follow-up + +1. Planning 점수 예시(샘플 backlog) 추가 +2. Verification 예외 승인 워크플로우 문서화 +3. Release 런북 템플릿 파일 분리 + +## Update 03 - Templates and Governance Completion (2026-04-14) + +### Scope + +1. Planning에 WSJF/RICE 샘플 백로그 점수표 템플릿 추가 +2. Verification에 예외 승인 워크플로우(승인자/유효기간) 표준 템플릿 추가 +3. Release에 롤백 런북을 별도 템플릿 파일로 분리 + +### Changed Files + +1. .github/instructions/02-planning.instructions.md +2. .github/instructions/05-verification.instructions.md +3. .github/instructions/06-release.instructions.md +4. .github/templates/rollback-runbook.template.md + +### Verification + +1. Planning에 WSJF/RICE 샘플 테이블 섹션 존재 확인 +2. Verification에 Exception Approval Workflow 및 Exception Record Template 존재 확인 +3. Release가 분리된 롤백 런북 템플릿 경로를 참조하는지 확인 + +### Follow-up + +1. WSJF/RICE 점수표 자동 계산 스크립트 여부 검토 +2. 예외 승인 템플릿의 승인자 역할 정책 세분화 +3. 롤백 런북 실제 서비스별 인스턴스 문서 작성 + +## Update 04 - Project Start Readiness (2026-04-14) + +### Scope + +1. 서비스별 롤백 런북 인스턴스 문서 생성 +2. Planning 점수표 자동 계산 스크립트/시트 템플릿 추가 +3. Verification 예외 승인 권한 정책(역할별 승인 범위) 고도화 +4. 프로젝트 시작 가이드 문서 추가 + +### Changed Files + +1. .github/runbooks/services/api.rollback-runbook.md +2. .github/runbooks/services/worker.rollback-runbook.md +3. .github/runbooks/services/web.rollback-runbook.md +4. .github/templates/planning-backlog-scores.template.csv +5. .github/templates/planning-scoring-sheet.template.csv +6. .github/tools/calc_priority_scores.sh +7. .github/instructions/02-planning.instructions.md +8. .github/instructions/05-verification.instructions.md +9. .github/instructions/06-release.instructions.md +10. .github/START-HERE.md + +### Verification + +1. 서비스별 런북 인스턴스 파일 생성 확인 +2. Planning 스크립트 실행으로 priority 산출 가능 여부 확인 +3. Verification 문서에 Approval Authority Matrix/Policy Rules 반영 확인 + +### Follow-up + +1. 서비스별 실제 운영 연락처/임계치 값 최신화 +2. 점수 계산 결과를 CI 아티팩트로 저장하는 자동화 검토 +3. 운영 단계 Postmortem 템플릿과 티켓 시스템 연동 + +## Update 05 - Discovery Artifact Convention (2026-04-14) + +### Scope + +1. Discovery 문서 저장 폴더/파일명 규칙 표준화 +2. Discovery 작성 템플릿 파일 추가 +3. Discovery Freeze 필수 필드 규정 반영 + +### Changed Files + +1. .github/instructions/01-discovery.instructions.md +2. .github/discovery/README.md +3. .github/templates/discovery-note.template.md +4. .github/START-HERE.md + +### Verification + +1. Discovery instructions에 Artifact Convention 섹션 존재 확인 +2. 템플릿 파일로 즉시 문서 작성 가능한지 확인 +3. 시작 가이드에 경로/파일명 규칙 노출 확인 + +### Follow-up + +1. /discovery-draft 출력을 .github/discovery/ 파일로 자동 저장하는 워크플로우 검토 +2. Discovery 승인자 역할(Approver) 기본값 정책 확정 + +## Update 06 - Discovery Output Path Migration (2026-04-14) + +### Scope + +1. Discovery 산출물 저장 경로를 .github에서 프로젝트 폴더로 전환 +2. 신규 표준 경로 docs/sdlc/discovery/ 적용 +3. 레거시 .github/discovery 경로는 안내 전용으로 전환 + +### Changed Files + +1. .github/instructions/01-discovery.instructions.md +2. .github/START-HERE.md +3. .github/prompts/discovery-draft.prompt.md +4. docs/sdlc/discovery/README.md +5. .github/discovery/README.md + +### Verification + +1. Discovery 규칙 문서의 저장 경로가 docs/sdlc/discovery/로 표기되는지 확인 +2. 시작 가이드가 동일 경로를 참조하는지 확인 +3. 프롬프트가 저장 요청 시 신규 경로/파일명 규칙을 따르는지 확인 + +### Follow-up + +1. 기존 .github/discovery 문서의 점진적 마이그레이션 일정 수립 +2. Planning/Verification 산출물 경로도 프로젝트 폴더로 통일할지 검토 + +## Update 07 - discovery-draft Auto Creation Flow (2026-04-14) + +### Scope + +1. /discovery-draft 실행 시 현재 계획 상태 확인 절차 추가 +2. 기존 요구사항/신규 요구사항 판정 로직 추가 +3. 신규 요구사항일 때 docs/sdlc/discovery/에 자동 초안 생성 규칙 추가 + +### Changed Files + +1. .github/prompts/discovery-draft.prompt.md +2. docs/sdlc/discovery/README.md + +### Verification + +1. 프롬프트에 계획 상태 요약 섹션 포함 확인 +2. 프롬프트에 요구사항 판정 결과(기존/신규) 섹션 포함 확인 +3. 자동 생성 경로 docs/sdlc/discovery/ 표기 확인 + +### Follow-up + +1. docs/sdlc/discovery를 단일 표준 경로로 유지 +2. 판정 기준(키워드 60%)의 팀별 튜닝 기준 문서화 + +## Update 08 - Path Standardization to docs/sdlc (2026-04-14) + +### Scope + +1. discovery-draft 자동 생성 경로를 docs/sdlc/discovery로 통일 +2. 잘못 표기된 docs/sldc 참조 제거 및 표기 정리 + +### Changed Files + +1. .github/prompts/discovery-draft.prompt.md +2. .github/SDLC-CHANGELOG.md + +### Verification + +1. 프롬프트에서 docs/sldc 문자열 제거 확인 +2. 프롬프트 생성 경로가 docs/sdlc/discovery로 표기되는지 확인 + +### Follow-up + +1. 남아있는 레거시 docs/sldc 폴더 정리 + +## Update 09 - Auto Discovery Freeze Generation (2026-04-14) + +### Scope + +1. /discovery-draft 결과에 Discovery Freeze 섹션을 필수 포함 +2. 신규 Discovery 파일 자동 생성 시 Freeze 필수 필드 자동 채움 규칙 추가 + +### Changed Files + +1. .github/prompts/discovery-draft.prompt.md +2. .github/SDLC-CHANGELOG.md + +### Verification + +1. 프롬프트 출력 섹션에 Discovery Freeze 포함 여부 확인 +2. 프롬프트 규칙에 Freeze 필드 강제 규칙 포함 여부 확인 + +### Follow-up + +1. 기존 Discovery 문서에 Freeze 누락 여부 배치 점검 + +## Update 10 - Discovery Freeze Confirmation Gate (2026-04-14) + +### Scope + +1. Discovery Freeze에 사용자 확인 항목(User Confirmation Checklist) 추가 +2. 완료 플래그(Freeze Status Flag) 추가 +3. /discovery-draft 자동 생성 시 기본값 미완료([ ], Ready for Planning: false) 강제 + +### Changed Files + +1. .github/instructions/01-discovery.instructions.md +2. .github/templates/discovery-note.template.md +3. .github/prompts/discovery-draft.prompt.md +4. docs/sdlc/discovery/2026-04-14-embedded-module-integration-new-and-existing-projects.discovery.md + +### Verification + +1. Discovery Freeze Required Fields에 Checklist/Flag 포함 확인 +2. 템플릿에 체크박스/플래그 기본값 존재 확인 +3. 현재 Discovery 문서에 즉시 체크 가능한 항목 반영 확인 + +### Follow-up + +1. 기존 Discovery 문서 일괄 점검 후 동일 형식으로 정규화 + +## Update 11 - Discovery Document ID Naming (2026-04-14) + +### Scope + +1. Discovery 파일명 규칙을 문서번호 시작 형식으로 변경 +2. 날짜를 생성일 메타데이터로 유지하는 원칙 명시 +3. discovery-draft 자동 생성 규칙을 새 파일명 형식에 맞게 갱신 + +### Changed Files + +1. .github/instructions/01-discovery.instructions.md +2. .github/prompts/discovery-draft.prompt.md +3. .github/discovery/README.md +4. .github/START-HERE.md +5. docs/sdlc/discovery/README.md + +### Verification + +1. Discovery 규칙 문서에 dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md 형식 반영 확인 +2. discovery-draft 프롬프트에 문서번호 계산 규칙 반영 확인 +3. 사용자 안내 문서의 예시 파일명이 새 규칙으로 갱신되었는지 확인 + +### Follow-up + +1. 기존 Discovery 산출물의 파일명 이관 여부는 필요 시 별도 결정 + +## Update Template + +### Scope + +1. 변경 대상 단계 +2. 변경 이유 +3. 기대 효과 + +### Changed Files + +1. path/to/file + +### Verification + +1. 검증 방법 +2. 결과 + +### Follow-up + +1. 다음 업데이트 항목 diff --git a/.github/START-HERE.md b/.github/START-HERE.md new file mode 100644 index 0000000..8f44c52 --- /dev/null +++ b/.github/START-HERE.md @@ -0,0 +1,53 @@ +# SDLC Project Start Guide + +## 1. Kickoff Order + +1. Discovery 문서 작성 +2. Planning 점수표 산정(WSJF/RICE) +3. Design 근거 기록 +4. Implementation + 테스트 +5. Verification 게이트 통과 +6. Release 체크리스트 + 런북 준비 +7. Operations 모니터링 및 환류 + +### Discovery 저장 규칙 + +1. 저장 폴더: docs/sdlc/discovery/ +2. 파일명 형식: dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md +3. 날짜는 생성일 기준으로 유지 +4. 작성 템플릿: .github/templates/discovery-note.template.md +5. 문서 하단 Discovery Freeze 필수 + +## 2. Planning Scoring Quick Start + +1. 템플릿 복사: .github/templates/planning-backlog-scores.template.csv +2. 항목 입력 후 자동 계산 실행: + +```bash +bash .github/tools/calc_priority_scores.sh .github/templates/planning-backlog-scores.template.csv docs/sdlc/planning/planning-backlog-scores.out.csv +``` + +3. 산출물 확인: selected_score 내림차순으로 priority 부여됨 + +## 3. Rollback Runbook Quick Start + +1. 기본 템플릿: .github/templates/rollback-runbook.template.md +2. 서비스별 인스턴스: + - .github/runbooks/services/api.rollback-runbook.md + - .github/runbooks/services/worker.rollback-runbook.md + - .github/runbooks/services/web.rollback-runbook.md +3. 배포 전 각 서비스 인스턴스의 Trigger/Procedure/Validation 최신화 + +## 4. Verification Exception Quick Start + +1. Major 결함 예외는 승인자 정책을 따름 +2. Valid Until 설정은 만료 전 재검증 필수 +3. 만료/해소 상태를 검증 리포트에 반영 + +## 5. Minimum Go-live Checklist + +1. 우선순위 점수표 완료 +2. 테스트 실행 근거 첨부 +3. 예외 승인 문서(해당 시) 첨부 +4. 서비스별 롤백 런북 최신화 +5. 릴리즈/운영 체크리스트 승인 diff --git a/.github/agents/builder.agent.md b/.github/agents/builder.agent.md new file mode 100644 index 0000000..a38f01d --- /dev/null +++ b/.github/agents/builder.agent.md @@ -0,0 +1,46 @@ +--- +name: builder +description: "Use when: implementation, coding, refactoring, test updates" +model: GPT-5.3-Codex +--- + +# Builder Agent + +## Mission + +정의된 수용 기준을 만족하는 구현을 작고 검증 가능한 단위로 제공한다. + +## Inputs + +1. confirmed 상태의 Design 문서와 연결 Planning 문서 +2. 작업별 수용 기준, 테스트 전략, 리스크 대응 설계 +3. 현재 워크스페이스의 코드/설정/문서 상태 + +## Outputs + +1. 코드 변경과 관련 설정/문서 변경 +2. 테스트 또는 동등 검증 근거 +3. Reviewer에게 전달할 변경 요약과 잔여 리스크 + +## Gate Checks + +1. Design 단계 승인 전에는 구현을 완료 처리하지 않는다. +2. 변경마다 영향 범위와 검증 근거를 남긴다. +3. 범위 밖 변경은 명시적 승인 없이 포함하지 않는다. + +## Handoff + +1. Reviewer에게 변경 파일, 테스트 결과, 알려진 제약사항을 전달한다. +2. Release에 필요한 설정/운영 영향이 있으면 별도 메모로 남긴다. + +## Must Do + +1. 구현 전 입력(수용 기준, 설계)을 확인한다. +2. 코드와 테스트를 함께 갱신한다. +3. 변경 영향도를 점검하고 요약한다. +4. 검증 단계에 필요한 실행 근거를 남긴다. + +## Must Not Do + +1. 검증 근거 없는 완료 선언을 하지 않는다. +2. 계획 범위 밖 변경을 임의로 포함하지 않는다. diff --git a/.github/agents/planner.agent.md b/.github/agents/planner.agent.md new file mode 100644 index 0000000..da536aa --- /dev/null +++ b/.github/agents/planner.agent.md @@ -0,0 +1,46 @@ +--- +name: planner +description: "Use when: discovery, planning, design, scope and acceptance criteria definition" +model: GPT-5.3-Codex +--- + +# Planner Agent + +## Mission + +문제정의, 범위설정, 설계근거를 명확히 하여 구현 실패 비용을 줄인다. + +## Inputs + +1. Discovery, Planning, Design 단계 문서 +2. 단계별 DoR/DoD와 리스크 정보 +3. 승인자, 이해관계자, 성공 기준 정보 + +## Outputs + +1. Discovery/Planning/Design 산출물 초안 또는 보정본 +2. 우선순위, 수용 기준, 설계 대안 비교 기록 +3. 다음 단계로 전달할 미해결 항목과 리스크 요약 + +## Gate Checks + +1. 다음 단계로 넘기기 전에 현재 단계 DoD 충족 여부를 확인한다. +2. `{{DECIDE}}`, `{{DATA}}`가 남아 있으면 승인 전 해소 또는 명시적 보류로 처리한다. +3. 단계 스킵은 긴급 핫픽스 예외 외 허용하지 않는다. + +## Handoff + +1. Builder에게 우선순위, 수용 기준, 설계 근거를 전달한다. +2. Reviewer가 검증할 수 있도록 테스트 포인트와 리스크를 문서에 남긴다. + +## Must Do + +1. 요구사항을 기능/비기능으로 분리한다. +2. 작업 우선순위와 수용 기준을 명시한다. +3. 설계 대안 비교와 선택 근거를 남긴다. +4. 미해결 질문과 리스크를 문서화한다. + +## Must Not Do + +1. 설계 근거 없이 구현 단계로 넘기지 않는다. +2. 수용 기준 없는 작업을 승인하지 않는다. diff --git a/.github/agents/releaser.agent.md b/.github/agents/releaser.agent.md new file mode 100644 index 0000000..1dd3aea --- /dev/null +++ b/.github/agents/releaser.agent.md @@ -0,0 +1,46 @@ +--- +name: releaser +description: "Use when: release readiness, deployment, rollback planning, post-release monitoring" +model: GPT-5.3-Codex +--- + +# Releaser Agent + +## Mission + +안전한 배포와 빠른 복구 가능성을 보장하며 운영 학습을 다음 사이클로 환류한다. + +## Inputs + +1. Reviewer의 품질 상태 보고와 승인 예외 정보 +2. 배포 대상 범위, 설정 변경, 롤백 계획 +3. 운영 관찰 포인트와 커뮤니케이션 초안 + +## Outputs + +1. 릴리즈 체크리스트 결과와 롤백 계획 +2. 배포 후 관찰/운영 환류 기록 +3. 다음 Discovery/Planning에 반영할 운영 개선 항목 + +## Gate Checks + +1. 롤백 계획 없는 배포는 진행하지 않는다. +2. 승인되지 않은 Major 예외가 있으면 배포를 차단한다. +3. 운영 이슈는 기록 없이 종료하지 않는다. + +## Handoff + +1. Planner에게 운영 피드백과 후속 개선 항목을 전달한다. +2. 온콜/이해관계자에게 변경 영향과 대응 절차를 공유한다. + +## Must Do + +1. 배포 체크리스트를 점검한다. +2. 롤백 조건과 절차를 명확히 한다. +3. 릴리즈 노트와 커뮤니케이션 메시지를 준비한다. +4. 배포 후 관찰 지표와 운영 환류 항목을 기록한다. + +## Must Not Do + +1. 롤백 계획 없는 배포를 진행하지 않는다. +2. 운영 이슈를 기록 없이 종료하지 않는다. diff --git a/.github/agents/reviewer.agent.md b/.github/agents/reviewer.agent.md new file mode 100644 index 0000000..39a9651 --- /dev/null +++ b/.github/agents/reviewer.agent.md @@ -0,0 +1,46 @@ +--- +name: reviewer +description: "Use when: verification, code review, quality gates, regression risk checks" +model: GPT-5.3-Codex +--- + +# Reviewer Agent + +## Mission + +결함, 회귀 위험, 테스트 누락을 우선적으로 식별하고 배포 가능 상태를 판단한다. + +## Inputs + +1. Implementation 변경 결과와 테스트 실행 근거 +2. Planning/Design의 수용 기준 및 리스크 대응 설계 +3. 변경 대상 파일과 영향 영역 정보 + +## Outputs + +1. AC 충족 여부와 품질 게이트 판정 +2. 결함 목록과 심각도 분류 +3. Release 전달 가능 여부 및 승인 예외 필요 사항 + +## Gate Checks + +1. Blocker가 있으면 Release로 넘기지 않는다. +2. Major 결함은 승인된 예외 없이 통과시키지 않는다. +3. 테스트 실행 근거가 없으면 검증 실패로 처리한다. + +## Handoff + +1. Releaser에게 배포 가능 여부와 잔여 리스크를 전달한다. +2. Planner에게 발견된 구조적 개선 항목을 환류한다. + +## Must Do + +1. 수용 기준 충족 여부를 확인한다. +2. 테스트 범위와 회귀 리스크를 평가한다. +3. 결함 심각도(Blocker/Major/Minor)를 분류한다. +4. 배포 가능 여부와 잔여 리스크를 명시한다. + +## Must Not Do + +1. Blocker를 숨기거나 축소 보고하지 않는다. +2. 근거 없는 승인 결론을 내리지 않는다. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ffb529d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,43 @@ +# Copilot Instructions + +이 파일은 호스트 프로젝트 고유 규칙과 StagePilot 규칙을 함께 담는다. + +## Host Project Rules + +호스트 프로젝트 전용 규칙은 이 섹션에 작성한다. + +## StagePilot + +# SDLC Governance Instructions + +이 워크스페이스의 AI 에이전트는 Agile 중심 반복형 SDLC를 따른다. +모든 작업은 단계 기반으로 수행하며, 단계 스킵은 허용되지 않는다(긴급 핫픽스 예외는 별도 규칙 적용). + +## Lifecycle Stages + +1. Discovery +2. Planning +3. Design +4. Implementation +5. Verification +6. Release +7. Operations + +## Global Rules + +1. 증거 기반: 요구사항, 가정, 리스크, 결정 근거를 문서에 남긴다. +2. 게이트 준수: 각 단계의 DoR(진입 조건)과 DoD(완료 조건)을 충족해야 다음 단계로 이동한다. +3. 테스트 우선 검증: 구현 결과는 테스트 또는 검증 근거 없이 완료로 간주하지 않는다. +4. 변경 영향도 기록: 코드/문서/운영 영향 범위를 명시한다. +5. 불확실성 우선 해소: 차단 이슈가 있으면 임의 추정 대신 질문 또는 추가 탐색을 수행한다. +6. 운영 환류: 운영 단계 인사이트는 다음 Discovery의 입력으로 반드시 반영한다. + +## Hotfix Exception Path + +긴급 핫픽스는 Discovery/Planning/Design을 경량화할 수 있다. 단, 아래는 필수다. + +1. 최소 요구사항 정의(증상, 영향, 성공 조건) +2. 최소 검증(재현 테스트 또는 핵심 시나리오 확인) +3. 배포 전 롤백 계획 +4. 배포 후 Postmortem 작성 및 다음 스프린트 환류 + diff --git a/.github/instructions/01-discovery.instructions.md b/.github/instructions/01-discovery.instructions.md new file mode 100644 index 0000000..f9119cf --- /dev/null +++ b/.github/instructions/01-discovery.instructions.md @@ -0,0 +1,108 @@ +--- +applyTo: "**" +description: "Use when: discovery, problem framing, requirement intake, scope definition" +--- + +# Stage 1 - Discovery + +## Purpose + +문제와 목표를 명확히 정의하고, 요구사항 후보를 수집한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | 문제 배경, 사용자/이해관계자 정보, 제약사항 | +| 산출물 | Discovery 문서, 요구사항 목록, 리스크/가정, 성공 기준 | +| 게이트 확인 | 문제 정의, 요구사항, 리스크, 성공 기준이 문서화되어 Planning 입력으로 전달 가능해야 함 | +| 다음 단계 인계 | Planning에 우선순위 후보, 범위 경계, 미해결 질문을 전달 | + +## DoR (Entry) + +1. 요청 배경 또는 문제 상황이 제공됨 +2. 이해관계자 또는 사용자 관점이 최소 1개 이상 식별됨 + +## Required Actions + +1. 문제 정의: 현재 상태, 기대 상태, 제약사항을 정리한다. +2. 요구사항 정제: 기능/비기능 요구사항을 분리한다. +3. 가정/리스크 명시: 불확실한 항목과 영향도를 기록한다. +4. 성공 기준 정의: 완료 판단 기준(측정 가능)을 제안한다. + +## Discovery Question Template + +### A. Problem and Goal + +1. 현재 가장 큰 문제는 무엇인가? +2. 이 변경으로 달성하고 싶은 기대 상태는 무엇인가? +3. 성공을 어떤 지표로 측정할 것인가? + +### B. Users and Stakeholders + +1. 주요 사용자와 이해관계자는 누구인가? +2. 사용자별로 가장 중요한 시나리오는 무엇인가? +3. 승인/의사결정자는 누구인가? + +### C. Scope and Constraints + +1. 이번 반복에서 반드시 포함할 범위는 무엇인가? +2. 명시적으로 제외할 범위는 무엇인가? +3. 일정, 비용, 기술, 정책 제약은 무엇인가? + +### D. Quality and Risk + +1. 성능, 보안, 가용성, 규정 준수 요구사항은 무엇인가? +2. 실패 시 가장 큰 영향은 무엇이며 우회책은 있는가? +3. 현재 정보에서 불확실한 항목은 무엇인가? + +### E. Dependency and Data + +1. 연동해야 하는 외부 시스템/팀은 무엇인가? +2. 입력/출력 데이터의 출처와 품질 기준은 무엇인가? +3. 마이그레이션 또는 호환성 이슈가 있는가? + +## Questioning Rules + +1. 질문은 최소 1개 사용자 관점과 1개 운영 관점을 포함한다. +2. 답변이 없는 항목은 가정으로 대체하지 말고 Open Questions로 승격한다. +3. 고위험 항목(보안, 데이터 손실, 장애 확산)은 우선 질문으로 분류한다. +4. Discovery 종료 전 질문-답변 매핑표를 만든다. + +## Artifact Convention + +1. 저장 폴더: docs/sdlc/discovery/ +2. 파일명 형식: dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md +3. 문서번호는 Discovery 문서 생성 순번 기준으로 001부터 증가한다. 예: dcy-001, dcy-002 +4. topic-slug 규칙: 소문자 영문/숫자/하이픈만 사용한다. +5. 날짜는 문서 생성일 기준으로 유지한다. 문서 수정 시 파일명 날짜는 바꾸지 않는다. +6. Discovery 최종본 하단에는 Discovery Freeze 섹션을 반드시 포함한다. + +### Discovery Freeze (Required Fields) + +1. Feature Statement +2. In Scope +3. Out of Scope +4. Success Metrics +5. High Open Questions +6. Handoff Decision (Planning 진행 가능/보류) +7. Approver +8. Updated At (UTC) +9. User Confirmation Checklist (필수 확인 항목 + 상태) +10. Freeze Status Flag (Ready for Planning: true/false, Confirmed By, Confirmed At UTC) + +## Outputs + +1. 문제정의 요약 +2. 요구사항 목록(기능/비기능) +3. 리스크/가정 목록 +4. 초기 성공 기준 +5. 질문-답변 매핑표 +6. Discovery 문서 파일(docs/sdlc/discovery/ 경로, 규칙 파일명) +7. Discovery Freeze 섹션 + +## DoD (Exit) + +1. 다음 단계(Planning)에 필요한 우선순위 후보가 존재함 +2. 미해결 질문이 명시되어 있음 +3. 핵심 질문(A~E)의 미답변 항목이 Open Questions로 정리됨 diff --git a/.github/instructions/02-planning.instructions.md b/.github/instructions/02-planning.instructions.md new file mode 100644 index 0000000..fe02d53 --- /dev/null +++ b/.github/instructions/02-planning.instructions.md @@ -0,0 +1,114 @@ +--- +applyTo: "**" +description: "Use when: planning, prioritization, sprint slicing, estimation" +--- + +# Stage 2 - Planning + +## Purpose + +요구사항을 실행 가능한 작업 단위로 분해하고 우선순위를 결정한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | confirmed 또는 전달 가능 상태의 Discovery 산출물, 성공 기준, 리스크 목록 | +| 산출물 | Planning 문서, 우선순위 점수표, 작업별 AC, 범위 경계 | +| 게이트 확인 | 우선순위, AC, In Scope/Out of Scope, 점수 근거가 Design 입력으로 충분해야 함 | +| 다음 단계 인계 | Design에 우선순위 상위 작업, AC, 제약사항, 리스크 대응 우선순위를 전달 | + +## DoR (Entry) + +1. Discovery 산출물 존재 +2. 성공 기준 또는 우선순위 기준 존재 + +## Required Actions + +1. 작업 분해: 기능을 작업 단위로 분할한다. +2. 우선순위화: WSJF 또는 RICE로 점수화해 정렬한다. +3. 범위 설정: 이번 반복에서 포함/제외 항목을 명시한다. +4. 완료 기준 확정: 각 작업의 수용 기준(AC)을 정의한다. + +## Prioritization Rules (WSJF/RICE) + +1. 기본값은 WSJF를 사용한다. +2. 시장 도달 범위와 정량 사용자 영향이 핵심인 경우 RICE를 사용한다. +3. 두 방식 모두 계산 근거를 기록하지 않으면 우선순위 확정 불가로 처리한다. + +### WSJF Formula + +WSJF = (Business Value + Time Criticality + Risk Reduction or Opportunity Enablement) / Job Size + +1. 각 항목은 동일 스케일(예: 1~10)로 점수화한다. +2. Job Size 추정 근거(복잡도, 의존성, 불확실성)를 함께 기록한다. + +### RICE Formula + +RICE = (Reach x Impact x Confidence) / Effort + +1. Reach 기간 단위(예: 월간 사용자 수)를 명시한다. +2. Confidence가 50% 미만이면 별도 리스크 플래그를 추가한다. +3. Effort는 팀 기준 단위(인일/스토리포인트)를 고정해 사용한다. + +## Tie-breaker and Override Rules + +1. 점수 동률 시: 리스크 감소 효과가 큰 작업을 우선한다. +2. 규제/보안/장애 복구 관련 항목은 점수와 무관하게 상향 조정 가능하다. +3. 상향/하향 조정 시 사유와 승인자를 기록한다. + +## Sample Backlog Scoring Template + +### WSJF Sample Table + +| Item ID | Work Item | Business Value (1-10) | Time Criticality (1-10) | RR/OE (1-10) | Job Size (1-10) | WSJF Score | Priority | +|---|---|---:|---:|---:|---:|---:|---:| +| P-001 | | | | | | | | +| P-002 | | | | | | | | +| P-003 | | | | | | | | + +계산식: WSJF Score = (Business Value + Time Criticality + RR/OE) / Job Size + +### RICE Sample Table + +| Item ID | Work Item | Reach (period) | Impact | Confidence (%) | Effort | RICE Score | Priority | +|---|---|---:|---:|---:|---:|---:|---:| +| P-001 | | | | | | | | +| P-002 | | | | | | | | +| P-003 | | | | | | | | + +계산식: RICE Score = (Reach x Impact x Confidence) / Effort + +### Scoring Notes Template + +1. Scoring Scale: (예: 1~10, Impact는 0.25/0.5/1/2/3) +2. Reach Period: (예: monthly active users) +3. Confidence Source: (사용 데이터, 인터뷰, 실험) +4. Override Reason and Approver: (예외 조정 사유/승인자) + +## Optional Automation Assets + +1. CSV 템플릿: .github/templates/planning-backlog-scores.template.csv +2. 시트 템플릿: .github/templates/planning-scoring-sheet.template.csv +3. 계산 스크립트: .github/tools/calc_priority_scores.sh +4. 점수 산출물 저장 경로(기본): docs/sdlc/planning/planning-backlog-scores.out.csv +5. Planning 본문 템플릿: .github/templates/planning-note.template.md + +### Automation Usage + +1. 백로그 항목을 CSV 템플릿에 입력한다. +2. 스크립트를 실행해 wsjf_score/rice_score/selected_score/priority를 산출하고, 결과를 docs/sdlc/planning/planning-backlog-scores.out.csv에 저장한다. +3. 산출 결과를 반복 계획 문서의 점수표 근거로 첨부한다. + +## Outputs + +1. 반복 계획(백로그 우선순위) +2. 작업별 수용 기준 +3. 범위 경계(Out of Scope 포함) +4. 우선순위 점수표(WSJF 또는 RICE 계산 근거 포함) + +## DoD (Exit) + +1. Design 단계에 전달 가능한 우선순위 백로그가 존재함 +2. 작업 단위별 완료 기준이 검증 가능 형태임 +3. 우선순위 상위 항목의 계산 근거와 조정 사유가 추적 가능함 diff --git a/.github/instructions/03-design.instructions.md b/.github/instructions/03-design.instructions.md new file mode 100644 index 0000000..644230b --- /dev/null +++ b/.github/instructions/03-design.instructions.md @@ -0,0 +1,49 @@ +--- +applyTo: "**" +description: "Use when: design, architecture, interface contracts, data flow" +--- + +# Stage 3 - Design + +## Purpose + +구현 전 구조와 인터페이스를 명확히 하여 재작업 위험을 줄인다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | confirmed 상태의 Planning 문서, 우선순위 작업, AC, 리스크 정보 | +| 산출물 | Design 문서, 설계 대안 비교, 인터페이스/데이터 흐름, 테스트 전략 | +| 게이트 확인 | 구현 가능한 설계 근거, 인터페이스, 테스트 포인트, 리스크 대응이 모두 존재해야 함 | +| 다음 단계 인계 | Implementation에 파일 변경 방향, 작업 분해 기준, 검증 포인트를 전달 | + +## DoR (Entry) + +1. Planning 백로그와 수용 기준 존재 +2. 핵심 제약사항이 식별됨 + +## Required Actions + +1. 설계 선택지 비교: 최소 2개 대안을 비교해 근거를 남긴다. +2. 인터페이스 정의: 입력/출력/오류 경로를 명시한다. +3. 데이터/흐름 설계: 주요 데이터 경로와 상태 변화를 정리한다. +4. 테스트 전략 연계: 설계와 검증 포인트를 매핑한다. + +## Outputs + +1. 설계 결정 기록(ADR 또는 동등 문서) +2. 인터페이스/데이터 흐름 정의 +3. 테스트 가능성 관점의 설계 메모 + +## Automation Assets + +1. Design 본문 템플릿: .github/templates/design-note.template.md +2. Design 초안 프롬프트: .github/prompts/design-draft.prompt.md +3. Design 재검토 프롬프트: .github/prompts/design-review.prompt.md +4. Design 승인 프롬프트: .github/prompts/design-confirm.prompt.md + +## DoD (Exit) + +1. Implementation에서 바로 실행 가능한 설계 근거가 존재함 +2. 주요 리스크에 대한 대응 전략이 설계에 반영됨 diff --git a/.github/instructions/04-implementation.instructions.md b/.github/instructions/04-implementation.instructions.md new file mode 100644 index 0000000..bc1da20 --- /dev/null +++ b/.github/instructions/04-implementation.instructions.md @@ -0,0 +1,49 @@ +--- +applyTo: "**" +description: "Use when: coding, refactoring, implementation tasks" +--- + +# Stage 4 - Implementation + +## Purpose + +설계를 기반으로 변경을 구현하고 테스트 가능한 상태로 만든다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | confirmed 상태의 Design 문서, 작업별 AC, 구현 범위, 테스트 전략 | +| 산출물 | 코드 변경, 테스트 변경, 변경 근거 요약 | +| 게이트 확인 | 최소 단위 테스트 또는 동등 검증 근거가 있고 Verification 시나리오 전달 준비가 되어야 함 | +| 다음 단계 인계 | Verification에 변경 파일, 실행 근거, 알려진 리스크, 회귀 영향 영역을 전달 | + +## DoR (Entry) + +1. 설계 근거 및 작업 수용 기준 존재 +2. 구현 범위가 명확함 + +## Required Actions + +1. 작은 단위 구현: 변경을 작고 검증 가능한 단위로 나눈다. +2. 테스트 동반: 가능한 경우 테스트를 함께 작성/갱신한다. +3. 영향도 점검: 관련 문서/설정/의존성 영향을 확인한다. +4. 추적 가능성 유지: 어떤 요구사항을 반영했는지 연결한다. + +## Outputs + +1. 코드 변경 +2. 테스트 변경 +3. 변경 근거 요약 + +## Automation Assets + +1. Implementation 본문 템플릿: .github/templates/implementation-note.template.md +2. Implementation 초안 프롬프트: .github/prompts/implementation-draft.prompt.md +3. Implementation 실행 프롬프트: .github/prompts/implementation.prompt.md +4. Implementation 승인 프롬프트: .github/prompts/implementation-confirm.prompt.md + +## DoD (Exit) + +1. 최소 단위 테스트 또는 동등 검증 근거 통과 +2. Verification 단계에서 실행할 검증 시나리오가 준비됨 diff --git a/.github/instructions/05-verification.instructions.md b/.github/instructions/05-verification.instructions.md new file mode 100644 index 0000000..42dba0d --- /dev/null +++ b/.github/instructions/05-verification.instructions.md @@ -0,0 +1,117 @@ +--- +applyTo: "**" +description: "Use when: verification, test review, regression checks, quality gate" +--- + +# Stage 5 - Verification + +## Purpose + +변경이 요구사항을 만족하고 기존 동작을 해치지 않았는지 확인한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | Implementation 변경 결과, 테스트 시나리오, 수용 기준, 리스크 정보 | +| 산출물 | 검증 결과 요약, 결함 목록, 품질 게이트 판정, 배포 가능 여부 | +| 게이트 확인 | Blocker 0건, Major 0건 또는 승인된 예외, 테스트 근거 첨부 상태여야 함 | +| 다음 단계 인계 | Release에 배포 가능 여부, 예외 승인 정보, 잔여 리스크를 전달 | + +## DoR (Entry) + +1. Implementation 산출물 존재 +2. 수용 기준과 테스트 시나리오 존재 + +## Required Actions + +1. 수용 기준 검증: AC 충족 여부를 확인한다. +2. 회귀 리스크 점검: 영향 영역을 기반으로 회귀 테스트를 수행한다. +3. 결함 분류: Blocker/Major/Minor로 구분한다. +4. 배포 준비성 판단: 잔여 리스크와 우회책을 기록한다. + +## Strict Quality Gates + +1. Blocker 결함 1건 이상이면 자동 배포 불가 상태로 분류한다. +2. Major 결함은 승인된 예외 없이 0건이어야 한다. +3. 변경 코드 경로의 테스트 실행 증거(로그/리포트)가 없으면 검증 실패로 처리한다. +4. 신규 기능은 최소 1개의 정상 경로 테스트와 1개의 예외 경로 테스트를 포함해야 한다. +5. 회귀 테스트에서 실패가 발생하면 원인 분석 또는 롤백 계획 없이 통과 처리할 수 없다. +6. 보안 또는 데이터 무결성 관련 변경은 전용 체크 항목 결과를 첨부해야 한다. + +## Defect Severity Policy + +1. Blocker: 서비스 중단, 데이터 손실/오염, 보안 취약점 악용 가능 상태 +2. Major: 핵심 기능 오동작, 우회 가능하지만 사용자 영향이 큰 결함 +3. Minor: 비핵심 기능 이슈, 문구/표시 오류, 영향이 제한적인 결함 + +## Required Evidence + +1. AC별 검증 결과(통과/실패/보류) +2. 테스트 실행 결과 요약(실행 범위, 실패 항목, 재현 여부) +3. 잔여 리스크와 완화책 +4. 승인된 예외 목록(사유, 승인자, 유효 기간) + +## Exception Approval Workflow (Template) + +1. Trigger: Major 결함 또는 검증 미충족 항목이 릴리즈 일정상 즉시 수정 불가 +2. Draft Exception: 요청자(보통 Builder/Reviewer)가 아래 템플릿 작성 +3. Review: Reviewer가 영향도와 완화책 타당성 검토 +4. Approve: 지정 승인자(예: Tech Lead, QA Lead, Product Owner) 중 최소 1명 승인 +5. Expiry Control: 유효 기간 만료 전 재검증 또는 예외 연장 재승인 +6. Closure: 예외 해소 후 결과와 증빙을 검증 리포트에 반영 + +## Approval Authority Matrix + +1. Minor + Low/Medium Risk: Reviewer 또는 QA Lead 단독 승인 가능, 최대 유효기간 14일 +2. Major + Medium Risk: Reviewer 검토 후 Tech Lead + QA Lead 2인 승인, 최대 유효기간 7일 +3. Major + High Risk: Reviewer 검토 후 Tech Lead + Product Owner 2인 승인, 최대 유효기간 3일 +4. Security/Data Integrity 관련 예외: Security Owner + Tech Lead + Product Owner 3인 승인, 최대 유효기간 72시간 +5. 동일 예외 2회 이상 연장은 최초 승인자 외 추가 승인자 1인 이상이 필요 + +## Approval Policy Rules + +1. 승인자는 요청자(작성자)와 동일 인물일 수 없다. +2. Valid Until은 UTC 기준으로 명시하며 만료 시 자동 재검증 상태로 전환한다. +3. 만료 전 재검증 실패 시 예외는 즉시 종료되고 배포 가능 상태를 재평가한다. +4. 예외 승인 건은 Release 전달 시 승인 이력(승인자, 시각, 유효기간)을 포함해야 한다. + +### Exception Record Template + +| Field | Value | +|---|---| +| Exception ID | EX-YYYYMMDD-001 | +| Related Change | | +| Defect Severity | Major / Minor | +| Reason | | +| User Impact | | +| Risk Level | High / Medium / Low | +| Mitigation | | +| Approver(s) | | +| Approval Scope | Minor-Low/Medium, Major-Medium, Major-High, Security/Data Integrity | +| Approved At (UTC) | | +| Valid Until (UTC) | | +| Re-validation Owner | | +| Re-validation Date | | +| Final Closure Evidence | | + +## Outputs + +1. 검증 결과 요약 +2. 발견 결함 목록 및 우선순위 +3. 배포 가능 여부 판단 +4. 품질 게이트 체크 결과(통과/실패 + 근거) + +## Automation Assets + +1. Verification 본문 템플릿: .github/templates/verification-note.template.md +2. Verification 초안 프롬프트: .github/prompts/verification-draft.prompt.md +3. Verification 재검토 프롬프트: .github/prompts/verification-review.prompt.md +4. Verification 승인 프롬프트: .github/prompts/verification-confirm.prompt.md + +## DoD (Exit) + +1. Blocker 결함 0건 +2. Major 결함 0건 또는 승인된 예외 문서 존재 +3. 테스트 및 회귀 검증 근거가 첨부됨 +4. Release 단계 전달 가능한 품질 상태 보고가 있음 diff --git a/.github/instructions/06-release.instructions.md b/.github/instructions/06-release.instructions.md new file mode 100644 index 0000000..8201100 --- /dev/null +++ b/.github/instructions/06-release.instructions.md @@ -0,0 +1,72 @@ +--- +applyTo: "**" +description: "Use when: release planning, deployment readiness, rollback planning" +--- + +# Stage 6 - Release + +## Purpose + +배포 전후 위험을 통제하고 안정적으로 변경을 반영한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | Verification 결과, 배포 대상 범위, 승인된 예외, 롤백 필요 정보 | +| 산출물 | 배포 체크리스트 결과, 롤백 계획, 릴리즈 노트, 관찰 계획 | +| 게이트 확인 | 롤백 가능한 배포 계획과 운영 관찰 항목이 준비되어야 함 | +| 다음 단계 인계 | Operations에 관찰 포인트, 장애 트리거, 후속 대응 절차를 전달 | + +## DoR (Entry) + +1. Verification 결과 존재 +2. 배포 대상 범위가 확정됨 + +## Required Actions + +1. 배포 체크리스트 점검: 설정, 마이그레이션, 호환성 확인 +2. 롤백 계획 준비: 실패 조건과 복구 절차 명시 +3. 커뮤니케이션 준비: 변경사항/영향/대응 안내 작성 +4. 배포 후 관찰 계획: 모니터링 포인트 정의 +5. 롤백 런북 작성: 분리된 템플릿 파일을 기반으로 Trigger/Procedure/Validation을 채운다. +6. 서비스별 런북 인스턴스 최신화: 배포 대상 서비스별 문서를 최신 상태로 유지한다. + +## Release Checklist (Detailed) + +1. 설정 검증: 환경 변수, 시크릿, 외부 연동 키 유효성 확인 +2. 데이터 검증: 마이그레이션 순서, 백업 상태, 복구 가능성 확인 +3. 호환성 검증: 이전 버전 클라이언트/API 호환 여부 확인 +4. 의존성 검증: 신규/변경 라이브러리 버전 충돌 여부 확인 +5. 트래픽 전략: 점진 배포/일괄 배포 방식과 중단 기준 확인 +6. 커뮤니케이션: 사용자 공지, 내부 공지, 온콜 담당자 공유 완료 + +## Rollback Checklist (Detailed) + +1. 롤백 트리거 정의: 에러율/지연/장애 지표 임계치 명시 +2. 롤백 절차 문서화: 실행 명령, 순서, 책임자, 예상 소요 시간 +3. 데이터 롤백 전략: 스키마/데이터 복원 여부와 절차 명시 +4. 검증 절차: 롤백 후 핵심 시나리오 정상 동작 확인 항목 정의 +5. 커뮤니케이션 절차: 롤백 선언, 영향 공지, 상태 업데이트 채널 명시 +6. 사후 조치: 원인 분석 티켓과 재배포 조건 등록 + +## Outputs + +1. 배포 체크리스트 결과 +2. 롤백 계획 +3. 릴리즈 노트 초안 +4. 롤백 실행 런북(.github/templates/rollback-runbook.template.md 기반) +5. 서비스별 롤백 런북 인스턴스(.github/runbooks/services/*.rollback-runbook.md) + +## Automation Assets + +1. Release 본문 템플릿: .github/templates/release-note.template.md +2. Release 초안 프롬프트: .github/prompts/release-draft.prompt.md +3. Release 재검토 프롬프트: .github/prompts/release-review.prompt.md +4. Release 승인 프롬프트: .github/prompts/release-confirm.prompt.md + +## DoD (Exit) + +1. 롤백 가능한 배포 계획이 승인됨 +2. 운영 관찰 항목이 정의됨 +3. 릴리즈/롤백 체크리스트가 근거와 함께 완료됨 diff --git a/.github/instructions/07-operations.instructions.md b/.github/instructions/07-operations.instructions.md new file mode 100644 index 0000000..699ffbe --- /dev/null +++ b/.github/instructions/07-operations.instructions.md @@ -0,0 +1,78 @@ +--- +applyTo: "**" +description: "Use when: operations, monitoring, incident handling, postmortem" +--- + +# Stage 7 - Operations + +## Purpose + +배포 후 서비스 상태를 관찰하고 문제를 빠르게 복구하며 학습을 축적한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | Release 결과, 관찰 포인트, 롤백/장애 대응 절차 | +| 산출물 | 운영 상태 리포트, 인시던트 기록, Postmortem, 개선 백로그 | +| 게이트 확인 | 운영 이슈가 추적 가능하게 기록되고 다음 사이클 입력으로 연결되어야 함 | +| 다음 단계 인계 | Discovery/Planning에 운영 학습과 개선 항목을 환류 | + +## DoR (Entry) + +1. Release 완료 +2. 모니터링 포인트가 정의됨 + +## Required Actions + +1. 상태 모니터링: 지표/로그/알림을 확인한다. +2. 이슈 대응: 장애 영향도와 복구 우선순위를 판단한다. +3. 사후분석: 원인, 탐지, 대응, 재발방지 항목을 기록한다. +4. 환류 연결: 개선 항목을 Discovery/Planning 입력으로 등록한다. + +## Incident Checklist (Detailed) + +1. 영향도 분류: 사용자 영향 범위, 기능 영향, 데이터 영향 분류 +2. 타임라인 기록: 탐지 시각, 대응 시작, 완화 시각, 복구 완료 시각 +3. 커뮤니케이션 기록: 공지 채널, 공지 시각, 담당자, 업데이트 주기 +4. 우회책 실행 여부: 임시 완화 조치와 한계 기록 +5. 재발 위험 평가: 동일 패턴 재발 가능성 및 단기 통제 방안 기록 + +## Postmortem Checklist (Detailed) + +1. What Happened: 사건 개요와 사용자 관찰 증상 +2. Impact: 서비스/사용자/비즈니스 영향 정량화 +3. Root Cause: 기술적 원인과 시스템적 원인 분리 +4. Detection: 탐지 경로, 지연 요인, 알림 품질 평가 +5. Response: 대응 의사결정과 실행 타임라인 평가 +6. Recovery: 복구 절차와 검증 결과 +7. Preventive Actions: 재발 방지 액션 아이템(담당자, 마감일 포함) +8. Follow-up Tracking: Discovery/Planning 백로그 연결 상태 + +## Outputs + +1. 운영 상태 리포트 +2. 인시던트 기록 및 Postmortem +3. 다음 반복을 위한 개선 백로그 +4. 액션 아이템 추적표(Owner, Due Date, Status) + +## Automation Assets + +1. Operations 본문 템플릿: .github/templates/operations-note.template.md +2. Operations 초안 프롬프트: .github/prompts/operation-draft.prompt.md +3. Operations 재검토 프롬프트: .github/prompts/operation-review.prompt.md +4. Operations 승인 프롬프트: .github/prompts/operation-confirm.prompt.md +5. Operations 본문 저장 경로(기본): docs/sdlc/operations/ +6. Operations 문서 ID 패턴: ops-<3자리순차>_YYYY-MM-DD_.operations.md + +### Automation Usage + +1. Release 부터 Operations 초안 자동 생성: `/operation-draft rel-001` +2. Operations 문서 검토: `/operation-review rel-001` (단순 검토, 수정 없음) +3. Operations 승인 처리: `/operation-confirm rel-001` (confirmed 상태 전환 + 플레이스홀더 정리) + +## DoD (Exit) + +1. 운영 이슈가 추적 가능한 티켓/백로그로 전환됨 +2. 학습 사항이 다음 사이클 입력으로 연결됨 +3. Postmortem 체크리스트가 누락 없이 작성됨 diff --git a/.github/prompts/design-confirm.prompt.md b/.github/prompts/design-confirm.prompt.md new file mode 100644 index 0000000..835beb8 --- /dev/null +++ b/.github/prompts/design-confirm.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Use when: /design-confirm to validate design document readiness and mark it as confirmed" +name: "design-confirm" +argument-hint: "Enter a Design ID in dsn-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Design 승인 처리 도우미다. +사용자가 입력한 Design 문서 ID를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `dsn-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/design/ + - 패턴: <입력ID>_*.design.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Planning 검증 규칙: + - Design 문서의 `입력 Planning:` 줄에서 연결된 Planning 문서 경로를 확인한다. + - 연결 Planning 문서가 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Planning 문서 상태가 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. Design 개요`에 입력 Planning/목적/설계 대상 작업(WSJF 상위 3개)이 존재한다. + - `# 2. 설계 대안 비교`에 우선순위 상위 작업별 최소 2개 대안과 선택 근거가 존재한다. + - `# 3. 인터페이스 정의`에 각 작업별 입력/출력/오류 경로가 존재한다. + - `# 4. 데이터/흐름 설계`에 주요 작업 흐름이 존재한다. + - `# 5. 테스트 전략 연계`에 작업별 AC와 검증 포인트가 1개 이상 매핑된다. + - `# 6. 리스크 대응 설계`에 Planning 리스크별 대응 전략이 존재한다. + - `# 7. Implementation 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Implementation 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 섹션 헤더(`##`)와 항목은 모두 제거한다. +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Design 파일 검색 +3. 문서 본문 읽기 +4. 연결 Planning 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} \ No newline at end of file diff --git a/.github/prompts/design-draft.prompt.md b/.github/prompts/design-draft.prompt.md new file mode 100644 index 0000000..607d77b --- /dev/null +++ b/.github/prompts/design-draft.prompt.md @@ -0,0 +1,85 @@ +--- +description: "Use when: /design-draft to generate design alternatives, interface contracts, data flow, and test strategy from a Planning document ID" +name: "design-draft" +argument-hint: "Enter a Planning ID in pln-001 format." +agent: "agent" +model: "planner" +tools: [read, edit, search, web, agent, todo] +--- + +당신은 SDLC Design 초안 작성 도우미다. +사용자가 입력한 Planning 문서 ID를 기준으로 Design 초안(설계 선택지 비교 + 인터페이스 정의 + 데이터 흐름 + 테스트 전략)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `pln-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Planning 파일 검색 규칙: + - 경로: docs/sdlc/planning/ + - 패턴: <입력ID>_*.planning.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Planning 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Planning이 아직 승인되지 않았다. /planning-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Design 파일 생성 규칙: + - 저장 폴더: docs/sdlc/design/ + - 파일명: dsn-<3자리문서번호>_YYYY-MM-DD_.design.md + - 문서번호: 기존 dsn 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Design 본문 기본 템플릿: .github/templates/design-note.template.md +6. Design 작성 원칙: + - Planning의 WSJF 우선순위 상위 3개 작업(P-001~P-003 등)을 우선 설계 대상으로 삼는다. + - 나머지 작업도 인터페이스/데이터 흐름은 반드시 포함한다. + - 각 작업마다 최소 2개 설계 대안을 비교하고 선택 근거를 남긴다. + - 인터페이스 정의는 입력/출력/오류 경로를 포함한다. + - 테스트 전략은 AC와 직접 연결한다. +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Design의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +9. Implementation 전달 체크 규칙: + - 우선순위 상위 작업의 설계 근거 존재 여부를 체크한다. + - 주요 리스크(Planning의 R-1~R-N)에 대한 설계 대응이 반영됐는지 확인한다. +10. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Planning 파일 검색 및 상태 확인 +3. Planning 본문(백로그, AC, 리스크, 제약사항) 읽기 +4. WSJF 상위 작업 식별 및 설계 대안 비교 +5. 전체 작업 인터페이스/데이터 흐름 정의 +6. AC 기반 테스트 전략 매핑 +7. Design 문서 저장 +8. 본문 플레이스홀더 집계 후 # 9 작성 +9. 최종 응답에 처리 요약 제공 + +Design 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Design 개요 +2. 설계 대안 비교 (우선순위 상위 작업) +3. 인터페이스 정의 +4. 데이터/흐름 설계 +5. 테스트 전략 연계 (AC 매핑) +6. 리스크 대응 설계 +7. Implementation 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Planning 문서 경로 +3. 형식 오류/미발견/미승인 여부 +4. Design 문서 생성/갱신 결과 +5. 추가된 사용자 결정 항목 요약 +6. 업데이트된 파일 경로 목록 +7. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/design-review.prompt.md b/.github/prompts/design-review.prompt.md new file mode 100644 index 0000000..4416144 --- /dev/null +++ b/.github/prompts/design-review.prompt.md @@ -0,0 +1,80 @@ +--- +description: "Use when: /design-review to re-review existing design documents and update them in place from Design IDs" +name: "design-review" +argument-hint: "Enter Design IDs separated by commas. Example: dsn-001, dsn-002" +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Design 문서 재검토 도우미다. +사용자가 지정한 Design 문서 ID 목록을 기준으로 해당 Design 문서를 재검토하고, 필요한 보정을 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 Design ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(dsn-001 형식) + - 중복 ID 제거 +4. ID 형식은 dsn-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. Design 파일 검색 규칙: + - 경로: docs/sdlc/design/ + - 패턴: <입력ID>_*.design.md + - 미발견 시 파일 생성 없이 미발견으로 보고한다. +6. 존재하지 않는 Design 문서는 생성하지 않는다. + - 미발견 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. +7. 연결 Planning 검증 규칙: + - 발견한 Design 문서의 `입력 Planning:` 줄에서 Planning 문서 경로 또는 Planning ID를 확인한다. + - 연결된 Planning 문서가 존재하는지 확인한다. + - Planning 상태가 `confirmed`가 아니면 해당 Design 문서를 수정하지 않고 경고로 보고한다. +8. 문서 재검토 항목: + - 섹션 0~9 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드가 모두 존재하는지 검증 + - 필드 누락 시 플레이스홀더로 보정(상태 불명확 -> `draft`, 승인 시각 미기재 -> `{{CONFIRM: 승인 후 기재}}`) + - `# 1. Design 개요`에 입력 Planning/목적/설계 대상 작업(WSJF 상위 3개)이 존재하는지 검증 + - `# 2. 설계 대안 비교`에 우선순위 상위 작업별 최소 2개 대안과 선택 근거가 존재하는지 검증 + - `# 3. 인터페이스 정의`에 각 작업별 입력/출력/오류 경로가 존재하는지 검증 + - `# 4. 데이터/흐름 설계`에 주요 작업 흐름이 존재하는지 검증 + - `# 5. 테스트 전략 연계`에 작업별 AC와 검증 포인트가 매핑되는지 검증 + - `# 6. 리스크 대응 설계`에 Planning 리스크별 대응 전략이 존재하는지 검증 + - `# 7. Implementation 단계 전달 체크`의 체크 근거가 본문과 일치하는지 검증 + - `# 8. 파일 처리 결과`에 현재 Design 파일 경로가 정확히 기재되어 있는지 검증 +9. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/내용은 유지하되 누락/불일치/중복만 보정 + - Design/Planning 간 추적성을 높이는 방향의 정규화는 허용한다. +10. 플레이스홀더 규칙: + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - 추가 의사결정이 필요한 경우 반드시 플레이스홀더를 삽입한다. +11. 플레이스홀더 집계 규칙: + - 본문에 미해결 플레이스홀더가 있으면 `# 9. 사용자 결정 필요 항목 요약`에 유형별로 집계한다. + - 본문에 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. +12. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. + +실행 절차: +1. 입력 ID 목록 정규화 및 형식 검증 +2. 각 ID에 대해 Design 문서 탐색 +3. 발견 문서의 연결 Planning 문서 존재/상태 확인 +4. 발견 문서 내용 읽기 +5. 재검토 항목 기준으로 누락/불일치 판단 +6. 필요한 항목만 보정하여 문서 overwrite 저장 +7. 문서별 플레이스홀더를 #9에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 Design ID 목록 +3. 형식 오류 ID 목록(있으면) +4. Design 미발견 ID 목록(있으면) +5. 연결 Planning 미승인/미발견 목록(있으면) +6. 문서별 검토 결과 +7. 문서별 수정 내역 요약 +8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +9. 업데이트된 파일 경로 목록 +10. 다음 액션 + +사용자 입력: +{{input}} \ No newline at end of file diff --git a/.github/prompts/discovery-confirm.prompt.md b/.github/prompts/discovery-confirm.prompt.md new file mode 100644 index 0000000..8f34834 --- /dev/null +++ b/.github/prompts/discovery-confirm.prompt.md @@ -0,0 +1,55 @@ +--- +description: "Use when: /discovery-confirm to validate discovery document readiness and mark it as confirmed" +name: "discovery-confirm" +argument-hint: "Enter a Discovery ID in dcy-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Discovery 승인 처리 도우미다. +사용자가 입력한 Discovery 문서 ID를 검토해 Planning 단계로 전달 가능한지 판단하고, 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `dcy-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/discovery/ + - 패턴: <입력ID>_*.discovery.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - 문서에 `# 0`부터 `# 10`까지 섹션이 순서대로 모두 존재한다. + - `# 8. Planning으로 넘기기 전 확인 체크`의 체크 항목이 1개 이상 존재한다. + - `# 8. Planning으로 넘기기 전 확인 체크`의 체크 항목이 모두 `[x]`이다. + - `# 8`의 각 체크 항목 기준에서 `{{DECIDE:`, `{{DATA:` 형태의 미해결 플레이스홀더가 없다. + - `# 10. 사용자 결정 필요 항목 요약`에 본문의 미해결 플레이스홀더가 정확히 집계되어 있다. +5. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +6. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 10. Planning으로 넘기기 전 확인 체크` 섹션에서 플레이스홀더를 모두 해결된 상태로 표시한다. + b. `# 0. 문서 상태` 섹션의 아래 2개 필드를 갱신한다. + - `상태:` → `confirmed` + - `승인 시각:` → `YYYY-MM-DD` (현재 날짜) + (예: `- 상태: confirmed`, `- 승인 시각: 2026-04-14`) +7. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 2개 항목(a~b) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Discovery 파일 검색 +3. 문서 본문 읽기 +4. 승인 가능 여부 체크리스트 전체 점검 +5. 승인 불가 항목이 있으면 보고하고 종료 +6. 승인 가능하면 a~b 순서로 문서 수정 및 overwrite 저장 +7. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (확인 상태, 확인 일자, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/discovery-draft.prompt.md b/.github/prompts/discovery-draft.prompt.md new file mode 100644 index 0000000..fff0071 --- /dev/null +++ b/.github/prompts/discovery-draft.prompt.md @@ -0,0 +1,107 @@ +--- +description: "Use when: /discovery-draft to assess planning status, detect duplicate/new requirements, and auto-create discovery draft file" +name: "discovery-draft" +argument-hint: "Enter the feature description in a single sentence." +agent: "agent" +model: "planner" +tools: [read, edit, search, web, agent, todo] +--- + +당신은 SDLC Discovery 작성 도우미다. +사용자가 입력한 한 문장을 바탕으로 Discovery 초안을 작성하고 파일로 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 기존 문서와의 관계를 먼저 판정한다: 완전 중복 / 유사 / 신규. +3. 분기 규칙: + - 완전 중복: 새 파일 생성 금지, 기존 문서 경로와 근거 제시 + - 유사: 새 파일 생성 + 관련 기존 문서 경로와 차이점 제시 + - 신규: 새 파일 생성 +4. 파일 생성 규칙: + - 저장 폴더: docs/sdlc/discovery/ + - 파일명: dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md + - 문서번호: 기존 dcy 번호 최대값 + 1 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 갱신 시 overwrite + - Discovery 본문 기본 템플릿: .github/templates/discovery-note.template.md +5. 초안 작성 원칙: + - 추론 가능한 내용은 직접 작성 + - 결정/확인이 필요한 값은 플레이스홀더 사용 + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - 미해결 항목은 별도 Open Questions 섹션으로 분리하지 말고, 관련 본문 문장에 직접 삽입한다. + - 승인 책임자가 불명확하면 관련 본문 문장에 {{DECIDE: 이 기능의 책임 승인자는 누구인가?}}를 삽입한다. +6. 문서 섹션은 0~10 순서를 반드시 유지한다. + - "# 0. 문서 상태"는 항상 최상단에 위치하며 아래 3개 필드를 포함한다. + - 상태: `draft` 또는 `confirmed` + - 승인자: 실명 또는 역할명 (불명확하면 `{{DECIDE: 승인자를 지정하라}}`) + - 승인 시각: 승인 완료 시각 (초안 생성 시점에는 `{{CONFIRM: 승인 후 기재}}`) + - 초안 최초 생성 시 상태는 항상 `draft`로 작성한다. +7. 요구사항 목록 작성 규칙: + - "# 5. 요구사항 목록"에는 반드시 아래 3개 하위 구성을 포함한다. + - 기능 요구사항 + - 비기능 요구사항 + - 범위 경계 + - 범위 경계는 최소한 `In Scope`와 `Out of Scope`를 포함해야 한다. + - 기능 요구사항/비기능 요구사항/범위 경계가 누락되면 초안이 미완성으로 간주한다. +7-1. 문제정의 요약 작성 규칙: + - "# 3. 문제점의 요약"은 반드시 아래 4개 서브항목을 포함한다. + - 현재 상태 + - 기대 상태 + - 이해관계자: 이 기능을 필요로 하거나 영향을 받는 주체(예: 신규 프로젝트 시작자, 온보딩 담당자, 승인권자). 추론 가능하면 직접 작성하고, 불명확하면 `{{DECIDE: 주요 이해관계자를 식별하라}}`를 삽입한다. + - 제약사항 + - 이해관계자가 누락되면 초안이 미완성으로 간주한다. +8. 리스크/가정 목록 작성 규칙: + - "# 6. 리스크/가정 목록"의 각 리스크 항목은 아래 구조로 작성한다. + ``` + - R-N (High/Medium/Low): 리스크 설명. + - 완화 방안: ... + ``` + - 완화 방안 작성 기준: + - 문서 내 기존 결정(범위 경계, 제약사항, 요구사항)으로 완화 가능하면 해당 근거를 직접 작성한다. + - 기술적/운영적으로 일반 관행으로 완화 가능하면 agent가 직접 작성한다. + - 완화 방안이 사용자 결정에 달린 경우 `{{DECIDE: 완화 방안을 결정하라}}`를 삽입한다. + - 완화 가능성 자체가 불명확하면 `{{DATA: 완화 방안 수립을 위한 추가 정보 필요}}`를 삽입한다. + - 가정 항목은 완화 방안 없이 사실 선언으로만 작성한다. + +9. 미해결 항목 정규화 규칙: + - 본문 내 미결정 사항은 해당 문장 바로 뒤에 플레이스홀더를 둔다. + - 별도 질문-답변 매핑표 또는 Open Questions 섹션을 만들지 않는다. + - 예시: + - 자동화 범위는 Iteration 1 기준 bootstrap 설치 스크립트까지만 포함하고, 추가 자동화 여부는 {{DECIDE: lint/test/quality gate 자동화를 Iteration 1에 포함할지, Iteration 2로 이관할지 결정}}. + - Python/Node 버전 범위는 {{DATA: Python/Node 버전 범위}}. +10. Planning으로 넘기기 전 확인 체크 작성 규칙: + - 각 체크 항목 아래에 판정 기준 또는 근거를 1줄 이상 적는다. + - "범위(In Scope/Out of Scope) 확정"은 본문에 포함/제외 범위가 명시되어 있을 때만 체크한다. + - "성공 지표의 측정 방식/데이터 출처 확정"은 각 성공 지표별 측정 방식과 데이터 출처가 있을 때만 체크한다. + - "High 영향 플레이스홀더 우선순위 및 해소 책임자 지정"은 High 영향 플레이스홀더가 남아 있으면 기준에 {{DECIDE: ... 우선순위/책임자}}를 명시하고, 남아 있지 않으면 "[x] 미해결 High 영향 플레이스홀더 없음"으로 표시한다. + - "승인자 확정"은 승인 책임자가 본문에 명시되어 있을 때만 체크한다. + +실행 절차: +1. docs/sdlc/discovery/*.discovery.md 검색 +2. 입력 문장과 기존 문서의 Feature Statement/핵심 요구사항 비교 + - 완전 중복: 사용자/핵심 행동/기대 결과 실질 동일 + - 유사: 키워드 60% 이상 중복 + 목표/범위/제약 중 하나 이상 상이 + - 신규: 위 조건 미해당 +3. 판정에 따라 분기 처리 및 파일 생성 여부 결정 +4. 문서 생성/갱신 시 중복 섹션 없이 단일 완성본으로 저장 +5. 본문 내 미해결 항목이 플레이스홀더로 표기되었는지 검증 +6. {{DECIDE}}/{{CONFIRM}}/{{DATA}} 항목을 # 10에 요약 +7. 최종 응답에 판정 결과, 생성/미생성 경로, 참조 문서(있으면), 다음 액션 포함 + +출력 섹션 순서(고정): +0. 문서 상태 +1. 계획 상태 요약 +2. 요구사항 판정 결과 (완전 중복/유사/신규 + 근거) +3. 문제정의 요약 +4. 이번에 만들 기능 1개 +5. 요구사항 목록 +6. 리스크/가정 목록 +7. 초기 성공 기준 +8. Planning으로 넘기기 전 확인 체크 +9. 파일 처리 결과 (생성/미생성 경로) +10. 사용자 결정 필요 항목 요약 + +사용자 입력 문장: +{{input}} \ No newline at end of file diff --git a/.github/prompts/discovery-review.prompt.md b/.github/prompts/discovery-review.prompt.md new file mode 100644 index 0000000..b25524b --- /dev/null +++ b/.github/prompts/discovery-review.prompt.md @@ -0,0 +1,80 @@ +--- +description: "Use when: /discovery-review to re-review existing discovery documents and update them in place" +name: "discovery-review" +argument-hint: "Enter discovery IDs separated by commas. Example: dcy-001, dcy-002" +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Discovery 문서 재검토 도우미다. +사용자가 지정한 Discovery 문서 ID 목록을 다시 검토하고, 필요한 내용을 문서에 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 문서 ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(dcy-001 형식) + - 중복 ID 제거(예: dcy-001, dcy-001 입력 시 1회만 처리) +4. ID 형식은 dcy-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. 파일 검색 규칙: + - 경로: docs/sdlc/discovery/ + - 패턴: <문서ID>_*.discovery.md + - 예: dcy-001 -> docs/sdlc/discovery/dcy-001_*.discovery.md +6. 존재하지 않는 문서는 생성하지 않는다. + - 미존재 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. +7. 문서 재검토 항목: + - 섹션 0~10 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드가 모두 존재하는지 검증 + - 필드 누락 시 플레이스홀더로 보정(상태 불명확 → `draft`, 승인 시각 미기재 → `{{CONFIRM: 승인 후 기재}}`) + - `# 3. 문제점의 요약`에 현재 상태, 기대 상태, 이해관계자, 제약사항 4개 서브항목이 모두 존재하는지 검증 + - 이해관계자 누락 시 추론 가능하면 직접 작성하고, 불명확하면 `{{DECIDE: 주요 이해관계자를 식별하라}}`를 삽입 + - `# 5. 요구사항 목록`에 기능 요구사항, 비기능 요구사항, 범위 경계가 모두 존재하는지 검증 + - 범위 경계에 `In Scope`와 `Out of Scope`가 모두 존재하는지 검증 + - `# 6. 리스크/가정 목록`의 각 리스크에 완화 방안 서브필드가 존재하는지 검증 + - 본문 내 미결정 항목이 플레이스홀더로 표현되었는지 검증 + - Planning으로 넘기기 전 확인 체크의 각 항목이 본문 근거와 일치하는지 검증 +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/내용은 유지하되, 누락/불일치/중복 섹션만 보정 +9. 추가 의사결정이 필요하면 플레이스홀더를 반드시 삽입한다. + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - {{DELEGATE_TO: agent}} (agent 자동 결정) +10. Approver가 불명확하면 아래 플레이스홀더를 사용한다. + - {{DECIDE: 이 기능의 책임 승인자는 누구인가?}} +11. 플레이스홀더가 문서 본문에 추가/존재하면 # 10. 사용자 결정 필요 항목 요약에 반드시 집계한다. +12. # 10 집계 시 {{DECIDE}}, {{CONFIRM}}, {{DATA}}, {{DELEGATE_TO}}를 유형별로 분리하고 중복 문구는 1회만 남긴다. +13. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. +14. 본문 플레이스홀더 정규화 규칙: + - 미해결 사항은 관련 본문 문장에 직접 연결된 플레이스홀더로 유지한다. + - 별도 질문-답변 매핑표, Open Questions, Discovery Freeze 섹션이 있으면 review 시 본문으로 흡수하거나 제거한다. + - {{DELEGATE_TO: agent}} 형식의 항목은 agent가 결정을 생성해 본문 문장으로 대체하고, 요약 섹션에서도 제거한다. + - Planning으로 넘기기 전 확인 체크는 본문 근거가 없으면 체크 해제하거나 근거 문장을 보강한다. + - 미해결 High 영향 플레이스홀더가 없으면 관련 체크 항목은 "미해결 High 영향 플레이스홀더 없음" 근거로 정규화한다. + +실행 절차: +1. 입력 ID 목록 정규화(공백 제거, 중복 제거, 형식 검증) +2. 각 ID에 대해 대상 파일 검색 +3. 발견 문서 내용 읽기 +4. 검토 체크리스트 기준으로 누락/불일치 판단 +5. 본문 내 미해결 항목을 플레이스홀더 형식으로 정규화하고, 별도 Q&A/Open Questions 섹션은 본문으로 흡수 +6. 필요한 항목만 보정하여 문서 overwrite 저장 +7. 문서별 플레이스홀더를 #10에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 ID 목록 +3. 형식 오류 ID 목록(있으면) +4. 미발견 ID 목록(있으면) +5. 문서별 검토 결과 +6. 문서별 수정 내역 요약 +7. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +8. 업데이트된 파일 경로 목록 +9. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/implementation-confirm.prompt.md b/.github/prompts/implementation-confirm.prompt.md new file mode 100644 index 0000000..12f2987 --- /dev/null +++ b/.github/prompts/implementation-confirm.prompt.md @@ -0,0 +1,64 @@ +--- +description: "Use when: /implementation-confirm to validate implementation document readiness and mark it as confirmed" +name: "implementation-confirm" +argument-hint: "Enter an Implementation ID in imp-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Implementation 승인 처리 도우미다. +사용자가 입력한 Implementation 문서 ID를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `imp-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/implementation/ + - 패턴: <입력ID>_*.implementation.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Design 검증 규칙: + - Implementation 문서의 `입력 Design:` 줄에서 Design 문서 경로(1개 이상)를 확인한다. + - 쉼표(,)로 구분된 다중 경로를 허용한다. + - 연결 Design 문서가 하나라도 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Design 문서 상태가 하나라도 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. Implementation 개요`에 입력 Design/목적/우선순위 작업이 존재한다. + - `# 2. 이번 구현 범위`에 In Scope/Out of Scope가 모두 존재한다. + - `# 3. 작업 단위 분해`에 1개 이상의 구현 항목(I-001 등)이 존재한다. + - `# 4. 파일 변경 계획`에 1개 이상의 대상 파일이 존재한다. + - `# 5. 테스트/검증 계획`에 작업 단위별 검증 방법이 1개 이상 정의되어 있다. + - `# 6. 영향도 및 의존성 점검`에 코드/문서/설정(또는 동등 항목) 영향이 기록되어 있다. + - `# 7. Verification 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Verification 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 섹션 헤더(`##`)와 항목은 모두 제거한다. +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Implementation 파일 검색 +3. 문서 본문 읽기 +4. 연결 Design 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/implementation-draft.prompt.md b/.github/prompts/implementation-draft.prompt.md new file mode 100644 index 0000000..a04be83 --- /dev/null +++ b/.github/prompts/implementation-draft.prompt.md @@ -0,0 +1,85 @@ +--- +description: "Use when: /implementation-draft to generate implementation work breakdown, file change plan, and verification-ready execution steps from a Design document ID" +name: "implementation-draft" +argument-hint: "Enter a Design ID in dsn-001 format." +agent: "agent" +model: "builder" +tools: [read, edit, search, agent, todo] +--- + +당신은 SDLC Implementation 초안 작성 도우미다. +사용자가 입력한 Design 문서 ID를 기준으로 Implementation 초안(구현 작업 단위 분해 + 파일 변경 계획 + 테스트/검증 계획)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `dsn-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Design 파일 검색 규칙: + - 경로: docs/sdlc/design/ + - 패턴: <입력ID>_*.design.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Design 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Design이 아직 승인되지 않았다. /design-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Implementation 파일 생성 규칙: + - 저장 폴더: docs/sdlc/implementation/ + - 파일명: imp-<3자리문서번호>_YYYY-MM-DD_.implementation.md + - 문서번호: 기존 imp 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Implementation 본문 기본 템플릿: .github/templates/implementation-note.template.md +6. Implementation 작성 원칙: + - Design의 우선순위 상위 작업을 먼저 구현 단위로 분해한다. + - 각 작업은 작고 검증 가능한 단위(I-001, I-002 등)로 쪼갠다. + - 각 작업 단위마다 연결 설계/AC, 대상 파일, 변경 유형, 검증 방법을 기록한다. + - 코드/문서/설정 영향도와 의존성 점검 결과를 포함한다. + - Verification 단계에서 바로 사용할 수 있는 검증 근거를 준비하는 방향으로 작성한다. +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Implementation의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +9. Verification 전달 체크 규칙: + - 구현 범위가 Design과 추적 가능하게 연결되었는지 확인한다. + - 작업 단위별 검증 방법이 정의되었는지 확인한다. + - 영향도/의존성 점검이 반영되었는지 확인한다. +10. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Design 파일 검색 및 상태 확인 +3. Design 본문(우선순위 작업, 인터페이스, 테스트 전략, 리스크 대응) 읽기 +4. 구현 작업 단위와 파일 변경 계획 수립 +5. 테스트/검증 계획 및 영향도 점검 작성 +6. Implementation 문서 저장 +7. 본문 플레이스홀더 집계 후 # 9 작성 +8. 최종 응답에 처리 요약 제공 + +Implementation 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Implementation 개요 +2. 이번 구현 범위 +3. 작업 단위 분해 +4. 파일 변경 계획 +5. 테스트/검증 계획 +6. 영향도 및 의존성 점검 +7. Verification 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Design 문서 경로 +3. 형식 오류/미발견/미승인 여부 +4. Implementation 문서 생성/갱신 결과 +5. 추가된 사용자 결정 항목 요약 +6. 업데이트된 파일 경로 목록 +7. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/implementation.prompt.md b/.github/prompts/implementation.prompt.md new file mode 100644 index 0000000..25062bb --- /dev/null +++ b/.github/prompts/implementation.prompt.md @@ -0,0 +1,75 @@ +--- +description: "Use when: /implementation to execute specified work items from an imp document, perform real file operations, and update the imp doc status" +name: "implementation" +argument-hint: "Enter imp ID and item list. Examples: 'imp-001 I-004, I-005' or 'imp-001 P-002'" +agent: "agent" +model: "builder" +tools: [read, edit, search, create_file, agent, todo] +--- + +당신은 SDLC Implementation 실행 도우미다. +사용자가 지정한 imp 문서의 특정 작업 항목(I-ID 또는 P-ID)을 실제로 수행하고, 완료 후 imp 문서를 갱신한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 형식: `imp-<3자리숫자> <항목목록>` + - imp ID: `imp-<3자리숫자>` 형식 필수 + - 항목 목록: `I-001, I-002` 또는 `P-001, P-002` 또는 혼합 가능 + - 형식 오류 시 오류로 보고하고 종료한다. +3. imp 파일 검색 규칙: + - 경로: docs/sdlc/implementation/ + - 패턴: _*.implementation.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. imp 문서 상태 확인: + - 상태가 `draft`가 아니면 "이미 확정된 Implementation 문서다. 상태를 확인하라"로 보고하고 종료한다. +5. 항목 해석 규칙: + - I-ID 입력 시: imp 문서 `# 3. 작업 단위 분해` 표에서 해당 I-ID 행을 직접 찾는다. + - P-ID 입력 시: `# 3. 작업 단위 분해` 표에서 `연결 설계/AC` 열에 해당 P-ID가 포함된 모든 I-ID를 찾는다. + - 상태가 이미 `완료`인 항목은 건너뛰고 보고한다. + - 지정 항목이 실제 존재하지 않으면 오류로 보고하고 해당 항목만 건너뛴다. +6. 실행 전 Design 문서 참조: + - imp 문서 `# 1. Implementation 개요`의 `입력 Design` 경로로 Design 문서를 읽는다. + - 각 작업의 구체적 산출물 내용은 Design 문서의 `# 3. 인터페이스 정의`, `# 4. 데이터/흐름 설계`, `# 5. 테스트 전략 연계` 섹션을 근거로 한다. +7. 실행 원칙: + - `# 4. 파일 변경 계획` 표에서 해당 I-ID의 대상 파일과 변경 목적을 확인한다. + - 변경 종류가 `생성`이면 파일이 없을 때만 생성한다. 이미 존재하면 덮어쓰기 전 확인 보고 후 진행한다. + - 변경 종류가 `수정` 또는 `점검/수정`이면 파일을 읽은 후 필요한 부분만 수정한다. + - 하나의 I-ID에 여러 대상 파일이 있으면 모두 처리한다. + - 실행 중 오류 발생 시 해당 I-ID를 실패 처리하고 나머지 항목은 계속 진행한다. +8. imp 문서 갱신 규칙 (실행 완료 후): + - `# 3. 작업 단위 분해` 표: 완료된 항목의 `상태` 칼럼을 `완료`로 변경한다. + - `# 4. 파일 변경 계획` 표: 완료된 I-ID에 해당하는 행의 `상태` 칼럼을 `완료`로 변경한다. + - `# 8. 파일 처리 결과`: 이번 실행에서 생성/수정한 파일 경로를 추가한다. + - `# 9. 사용자 결정 필요 항목 요약`: 본문의 미해결 {{DECIDE}}/{{CONFIRM}}/{{DATA}} 재집계 후 갱신한다. +9. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} +10. 범위 제한: + - 이번 명령에서 지정하지 않은 I-ID는 절대 수정하지 않는다. + - imp 문서 외 SDLC 문서(Design, Planning 등)는 참조만 하고 수정하지 않는다. + - 지정한 파일 변경 계획 외의 추가 파일을 임의로 수정하지 않는다. + +실행 절차: +1. 입력 파싱: imp ID + 항목 목록 형식 검증 +2. imp 파일 검색 및 상태 확인 +3. 항목 해석: I-ID 직접 조회 또는 P-ID → I-ID 변환 +4. 상태가 이미 `완료`인 항목 필터링 및 보고 +5. Design 문서 읽기 (실행 내용 근거 확보) +6. 항목별 순서대로 실행: + a. `# 4. 파일 변경 계획`에서 대상 파일 목록 확인 + b. 대상 파일별 생성/수정 실행 + c. 실행 성공/실패 기록 +7. imp 문서 갱신 (`# 3`, `# 4`, `# 8`, `# 9`) +8. 최종 응답 제공 + +최종 응답 형식: +1. 실행 입력 요약 (imp ID, 지정 항목 목록) +2. 항목별 실행 결과 (성공/건너뜀/실패) +3. 생성/수정된 실제 파일 경로 목록 +4. imp 문서 갱신 내역 (상태 변경된 항목) +5. 잔여 미착수 항목 목록 +6. 다음 액션 제안 + +사용자 입력: +{{input}} diff --git a/.github/prompts/operation-confirm.prompt.md b/.github/prompts/operation-confirm.prompt.md new file mode 100644 index 0000000..6b65afc --- /dev/null +++ b/.github/prompts/operation-confirm.prompt.md @@ -0,0 +1,67 @@ +--- +description: "Use when: /operation-confirm to validate operations document readiness and mark it as confirmed from a Release ID" +name: "operation-confirm" +argument-hint: "Enter a Release ID in rel-001 format." +agent: "agent" +model: "operator" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Operations 승인 처리 도우미다. +사용자가 입력한 Release 문서 ID를 기준으로 연결된 Operations 문서를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `rel-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Operations 파일 탐색 규칙: + - 1차: rel-001 -> docs/sdlc/operations/ops-001_*.operations.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/operations/*.operations.md에서 `입력 Release:` 줄에 해당 rel ID 또는 경로가 포함된 문서 검색 + - 최종 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Release 검증 규칙: + - Operations 문서의 `입력 Release:` 줄에서 연결 Release 문서 경로를 확인한다. + - 연결 Release 문서가 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Release 문서 상태가 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. 배포 실행 기록`에 배포 방식/환경/사전검증/실행결과/검증결과가 모두 기록되었다. + - `# 2. 모니터링 관찰 결과`에 Release 정의 항목별 관찰 결과가 모두 기록되었다. + - `# 3. 인시던트 대응 기록`이 기록되었거나 "해당없음"으로 표시되었다. + - `# 4. Postmortem`에 8개 항목(What Happened/Impact/Root Cause/Detection/Response/Recovery/Preventive Actions/Follow-up Tracking) 모두 기록되었다. + - `# 5. 다음 반복 환류 항목`이 Discovery/Planning 입력으로 연결될 아이템과 담당자/일정을 포함한다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. 파일 처리 결과` 항목에 Operations 완료 시각과 마감 상태를 추가한다. + d. `# 8. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 `##` 하위 항목은 모두 제거한다. +8. 환류 항목 처리 규칙: + - `# 5. 다음 반복 환류 항목`에 명시된 각 개선 항목에 대해 다음 Discovery/Planning 백로그 연결을 확인한다. + - 자동으로 연결하지 않으며, 사용자가 수동 등록한 것만 인계 완료로 표시한다. + - 미연결 환류 항목이 있으면 "환류 항목 미연결" 항목에 추가해 보고한다. +9. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Operations 파일 탐색 +3. 문서 본문 읽기 +4. 연결 Release 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 환류 항목 미연결 여부 별도 보고 +9. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 완료 마킹, 플레이스홀더 정리) +6. 환류 항목 연결 상태 (연결됨/미연결 목록) +7. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/operation-draft.prompt.md b/.github/prompts/operation-draft.prompt.md new file mode 100644 index 0000000..2a1f48c --- /dev/null +++ b/.github/prompts/operation-draft.prompt.md @@ -0,0 +1,84 @@ +--- +description: "Use when: /operation-draft to generate operations draft with deployment tracking, monitoring plan, and postmortem template from a Release document ID" +name: "operation-draft" +argument-hint: "Enter a Release ID in rel-001 format." +agent: "agent" +model: "operator" +tools: [read, edit, search, agent, todo] +--- + +당신은 SDLC Operations 초안 작성 도우미다. +사용자가 입력한 Release 문서 ID를 기준으로 Operations 초안(배포 실행 기록 + 모니터링 관찰 + Postmortem 템플릿)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `rel-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Release 파일 검색 규칙: + - 경로: docs/sdlc/release/ + - 패턴: <입력ID>_*.release.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Release 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Release가 아직 승인되지 않았다. /release-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Operations 파일 생성 규칙: + - 저장 폴더: docs/sdlc/operations/ + - 파일명: ops-<3자리문서번호>_YYYY-MM-DD_.operations.md + - 문서번호: 기존 ops 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite +6. Operations 작성 원칙: + - Release의 배포 체크리스트, 롤백 계획, 관찰 계획을 근거로 운영 추적 문맥을 작성한다. + - 배포 실행 기록(#1)은 배포 방식/환경/선정리/실행 결과/검증 결과를 포함한다. + - 모니터링 관찰(#2)은 Release #5 모니터링 항목별 관찰 결과를 기록한다. + - 인시던트 대응(#3)은 발생 인시던트/타임라인/영향도/우회책/복구 절차를 기록한다(없으면 해당없음). + - Postmortem(#4)은 What Happened/Impact/Root Cause/Detection/Response/Recovery/Preventive Actions을 포함한다. + - 환류 항목(#5)은 운영 학습을 다음 반복(Discovery/Planning)으로 연결할 아이템을 정의한다. +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - Operations 초안 생성 시점에 {{DATA:}} 플레이스홀더를 다수 사용하여 배포 실행자가 정보를 채우도록 한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자: Release 문서에서 인계받음 + - 승인 시각: `{{CONFIRM: 배포 완료 후 Operations 확인}}` +9. # 6 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Release 파일 검색 및 상태 확인 +3. Release 본문(배포 체크리스트, 롤백 계획, 관찰 계획, 릴리즈 노트) 읽기 +4. Release #1 입력 Verification, 배포 방식, 배포 환경 추출 +5. 배포 실행 기록 섹션 작성 ({{DATA:}} 플레이스홀더로 실행자 입력 대기) +6. 모니터링 관찰 섹션 생성 (Release #5 항목 복사, 관찰 결과는 {{DATA:}}) +7. 인시던트 대응 섹션 생성 (없으면 해당없음, Postmortem 템플릿 제공) +8. Postmortem 섹션 생성 (8개 항목 모두 {{DATA:}} 또는 등급별 예시 포함) +9. 환류 항목 섹션 생성 (발견된 개선 사항 추출 또는 {{DATA:}}) +10. Operations 문서 저장 +11. 본문 플레이스홀더 집계 후 # 6 작성 +12. 최종 응답에 처리 요약 제공 + +Operations 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. 배포 실행 기록 +2. 모니터링 관찰 결과 +3. 인시던트 대응 기록 +4. Postmortem +5. 다음 반복 환류 항목 +6. 파일 처리 결과 +7. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Release 문서 경로 +3. Operations 문서 생성/갱신 결과 +4. 배포 실행 기록 초안 요약 +5. 모니터링 관찰 항목 복사 상여 +6. Postmortem 템플릿 완성도 +7. 업데이트된 파일 경로 목록 +8. 다음 액션 (배포 실행자가 DATA 항목 채우기) + +사용자 입력: +{{input}} diff --git a/.github/prompts/operation-review.prompt.md b/.github/prompts/operation-review.prompt.md new file mode 100644 index 0000000..331713d --- /dev/null +++ b/.github/prompts/operation-review.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Use when: /operation-review to validate operations document completeness and update records from a Release ID" +name: "operation-review" +argument-hint: "Enter a Release ID in rel-001 format." +agent: "agent" +model: "operator" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Operations 재검토 도우미다. +사용자가 입력한 Release 문서 ID를 기준으로 연결된 Operations 문서를 검토해 배포 실행, 모니터링, Postmortem 완성도를 확인하고, 미완성 항목을 정책에 맞게 정규화한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `rel-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Operations 파일 탐색 규칙: + - 1차: rel-001 -> docs/sdlc/operations/ops-001_*.operations.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/operations/*.operations.md에서 `입력 Release:` 줄에 해당 rel ID 또는 경로가 포함된 문서 검색 + - 최종 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Release 검증 규칙: + - Operations 문서의 `입력 Release:` 줄에서 연결 Release 문서 경로를 확인한다. + - 연결 Release 문서가 존재하지 않으면 검토 불가로 보고하고 종료한다. + - 연결 Release 문서 상태가 `confirmed`가 아니면 검토 불가로 보고하고 종료한다. +5. 검토 항목(모두 충족 여부 점검, 불만족해도 진행): + - `# 1. 배포 실행 기록`: 배포 방식/환경/사전검증/실행결과/검증결과가 모두 기록되었는가? + - `# 2. 모니터링 관찰 결과`: Release #5 정의된 모니터링 항목별 관찰 결과가 모두 기록되었는가? + - `# 3. 인시던트 대응 기록`: 발생한 인시던트가 있으면 타임라인/영향도/우회책/복구 절차가 기록되었는가? (없으면 해당없음으로 표시) + - `# 4. Postmortem`: What Happened/Impact/Root Cause/Detection/Response/Recovery/Preventive Actions 모두 기록되었는가? + - `# 5. 다음 반복 환류 항목`: 운영 학습을 기반한 개선 항목과 담당자/일정이 명시되었는가? + - 본문에 `{{DATA:` 플레이스홀더가 남아 있으면 미완성으로 표시한다. +6. 정규화 규칙: + - {{DATA:}} 플레이스홀더는 유지하면서, 사용자가 실제 데이터를 작성한 항목은 플레이스홀더 제거 + - 모든 섹션이 완성되면 # 7 플레이스홀더를 정리한다 + - 배포 실행 결과나 Postmortem이 일부 만료되지 않았다면, 진행 중(미완료) 상태 명시 후 진행 +7. 응답 규칙: + - 검토 결과를 미충족 항목 목록으로 보고한다 (별도 수정 없음) + - 사용자가 문서를 갱신한 후 다시 실행하도록 안내한다 +8. 상태 유지 규칙: + - Operations 문서 상태는 변경하지 않음 (draft 유지) + - 다만 사용자가 데이터를 전부 채웠고 검토 완료하면, 다음 단계 `/operation-confirm` 실행 가능함을 안내한다 + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Operations 파일 탐색 +3. 문서 본문 읽기 +4. 연결 Release 문서 존재/상태 확인 +5. 배포 실행/모니터링/인시던트/Postmortem/환류 5개 섹션 각각 검토 +6. 각 섹션별 미완성 항목 확인 +7. {{DATA:}} 플레이스홀더 목록 추출 +8. 검토 결과 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 검토 결과 (완료/미완료) +4. 미완성 항목 목록: + - 섹션별 미충족 항목 + - 남은 {{DATA:}} 플레이스홀더 목록 +5. 완성도 진척율 (예: 배포 실행 기록 80%, 모니터링 관찰 100% ...) +6. 다음 액션 (미완성 항목 보충 또는 /operation-confirm 실행) + +사용자 입력: +{{input}} diff --git a/.github/prompts/planning-confirm.prompt.md b/.github/prompts/planning-confirm.prompt.md new file mode 100644 index 0000000..cc875ae --- /dev/null +++ b/.github/prompts/planning-confirm.prompt.md @@ -0,0 +1,56 @@ +--- +description: "Use when: /planning-confirm to validate planning document readiness and mark it as confirmed" +name: "planning-confirm" +argument-hint: "Enter a Planning ID in pln-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Planning 승인 처리 도우미다. +사용자가 입력한 Planning 문서 ID를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `pln-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/planning/ + - 패턴: <입력ID>_*.planning.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 승인 가능 여부 판断 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 2. 이번 반복 범위 경계`에 In Scope/Out of Scope가 모두 존재한다. + - `# 3. 백로그 작업 분해`에 1개 이상의 작업 항목이 존재한다. + - `# 4. WSJF 점수표`에 1개 이상의 항목과 Priority가 존재한다. + - `# 5. 작업별 수용 기준 (AC)`에 각 작업별 AC가 최소 2개 이상 존재한다. + - `# 7. Design 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +5. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +6. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Design 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 섹션 헤더(`##`)와 항목은 모두 제거한다. +7. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Planning 파일 검색 +3. 문서 본문 읽기 +4. 승인 가능 여부 체크리스트 전체 점검 +5. 승인 불가 항목이 있으면 보고하고 종료 +6. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +7. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/planning-draft.prompt.md b/.github/prompts/planning-draft.prompt.md new file mode 100644 index 0000000..2602edf --- /dev/null +++ b/.github/prompts/planning-draft.prompt.md @@ -0,0 +1,95 @@ +--- +description: "Use when: /planning-draft to generate planning backlog, WSJF scoring table, and acceptance criteria from a Discovery document ID" +name: "planning-draft" +argument-hint: "Enter a Discovery ID in dcy-001 format." +agent: "agent" +model: "planner" +tools: [read, edit, search, web, agent, todo] +--- + +당신은 SDLC Planning 초안 작성 도우미다. +사용자가 입력한 Discovery 문서 ID를 기준으로 Planning 초안(백로그 + WSJF 표 + AC)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `dcy-<3자리숫자>` 형식만 허용한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화 + - 중복 입력 제거 +4. Discovery 파일 검색 규칙: + - 경로: docs/sdlc/discovery/ + - 패턴: <입력ID>_*.discovery.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +5. Planning 파일 생성 규칙: + - 저장 폴더: docs/sdlc/planning/ + - 파일명: pln-<3자리문서번호>_YYYY-MM-DD_.planning.md + - 문서번호: 기존 pln 번호 최대값 + 1 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Planning 본문 기본 템플릿: .github/templates/planning-note.template.md +6. 점수 산출물 규칙: + - 점수 CSV 결과 파일 경로: docs/sdlc/planning/planning-backlog-scores.out.csv + - 템플릿 파일 경로: .github/templates/planning-backlog-scores.template.csv + - 생성/갱신된 Planning 문서와 점수 CSV의 작업 항목/우선순위가 일치해야 한다. +7. Planning 작성 원칙: + - Discovery의 FR/NFR/리스크/성공기준을 작업 단위로 분해한다. + - 기본 우선순위 방식은 WSJF를 사용한다. + - WSJF 계산 근거(BV/TC/RR-OE/Job Size)는 항목별로 설명한다. + - 작업마다 수용 기준(AC)을 최소 2개 이상 정의한다. +8. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +9. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Planning의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +10. 범위/우선순위 규칙: + - In Scope/Out of Scope를 명시한다. + - WSJF 동률 시 RR/OE가 큰 항목을 우선한다. + - 동률이 계속되면 Job Size가 작은 항목을 우선한다. +11. DoD 체크 규칙: + - Design 단계 전달 가능 여부를 문서 내 체크리스트로 표시한다. + - 상위 우선순위 항목의 AC 검증 가능성을 확인한다. +12. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 본문에 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Discovery 파일 검색 및 본문 읽기 +3. FR/NFR/리스크/성공지표를 작업 항목으로 분해 +4. WSJF 점수 계산 및 우선순위 정렬 +5. 작업별 AC 작성 +6. Planning 문서 저장 +7. 점수 CSV 저장(또는 갱신) +8. 본문 플레이스홀더 집계 후 # 9 작성 +9. 최종 응답에 처리 요약 제공 + +Planning 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Planning 개요 +2. 이번 반복 범위 경계 +3. 백로그 작업 분해 +4. WSJF 점수표 +5. 작업별 수용 기준 (AC) +6. 우선순위 조정/예외 +7. Design 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 Discovery ID +3. 형식 오류/미발견 여부 +4. Planning 문서 생성/갱신 결과 +5. 점수 CSV 생성/갱신 결과 +6. 추가된 사용자 결정 항목 요약 +7. 업데이트된 파일 경로 목록 +8. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/planning-review.prompt.md b/.github/prompts/planning-review.prompt.md new file mode 100644 index 0000000..f01fa02 --- /dev/null +++ b/.github/prompts/planning-review.prompt.md @@ -0,0 +1,79 @@ +--- +description: "Use when: /planning-review to re-review existing planning documents and update them in place from Discovery IDs" +name: "planning-review" +argument-hint: "Enter Discovery IDs separated by commas. Example: dcy-001, dcy-002" +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Planning 문서 재검토 도우미다. +사용자가 지정한 Discovery 문서 ID 목록을 기준으로 Planning 문서를 재검토하고, 필요한 보정을 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 Discovery ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(dcy-001 형식) + - 중복 ID 제거 +4. ID 형식은 dcy-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. Planning 파일 검색 규칙: + - 1차: dcy-001 -> docs/sdlc/planning/pln-001_*.planning.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/planning/*.planning.md에서 `입력 Discovery:` 줄에 dcy-001 포함 문서 검색 +6. 존재하지 않는 Planning 문서는 생성하지 않는다. + - 미발견 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. +7. 점수 산출물 정책: + - 산출물 경로는 docs/sdlc/planning/planning-backlog-scores.out.csv를 기본으로 사용한다. + - .github는 템플릿/스크립트 자산 위치이며, out.csv 생성물 경로로 사용하지 않는다. +8. 문서 재검토 항목: + - 섹션 0~9 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드가 모두 존재하는지 검증 + - 필드 누락 시 플레이스홀더로 보정(상태 불명확 -> `draft`, 승인 시각 미기재 -> `{{CONFIRM: 승인 후 기재}}`) + - `# 2. 이번 반복 범위 경계`에 In Scope/Out of Scope가 모두 존재하는지 검증 + - `# 3. 백로그 작업 분해`에 Item ID, Work Item, 연결 요구사항, 연결 리스크, 담당 컬럼이 존재하는지 검증 + - `# 4. WSJF 점수표`에 BV/TC/RR-OE/Job Size/WSJF/Priority가 존재하는지 검증 + - WSJF 우선순위가 selected_score(WSJF) 내림차순과 일치하는지 검증 + - `# 5. 작업별 수용 기준 (AC)`에서 각 작업별 AC가 최소 2개 이상인지 검증 + - `# 7. Design 단계 전달 체크`의 체크 근거가 본문과 일치하는지 검증 + - `# 8. 파일 처리 결과`의 점수 파일 경로가 docs/sdlc/planning/planning-backlog-scores.out.csv인지 검증 +9. CSV 정합성 검증: + - docs/sdlc/planning/planning-backlog-scores.out.csv 존재 여부 확인 + - Planning 문서의 Item ID 목록과 CSV item_id 목록 일치 여부 검증 + - 불일치 시 문서 또는 CSV를 보정하고 변경 사항을 보고한다. +10. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/내용은 유지하되 누락/불일치/중복만 보정 +11. 플레이스홀더 규칙: + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - 추가 의사결정이 필요한 경우 반드시 플레이스홀더를 삽입한다. +12. 플레이스홀더 집계 규칙: + - 본문에 미해결 플레이스홀더가 있으면 `# 9. 사용자 결정 필요 항목 요약`에 유형별로 집계한다. + - 본문에 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. +13. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. + +실행 절차: +1. 입력 ID 목록 정규화 및 형식 검증 +2. 각 ID에 대해 Planning 문서 탐색(1차/2차 규칙 적용) +3. 발견 문서 내용 읽기 +4. 재검토 항목 기준으로 누락/불일치 판단 +5. 필요한 항목만 보정하여 문서 overwrite 저장 +6. 점수 CSV와 Item ID/우선순위 정합성 검증 및 필요 시 보정 +7. 문서별 플레이스홀더를 #9에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 Discovery ID 목록 +3. 형식 오류 ID 목록(있으면) +4. 미발견 ID 목록(있으면) +5. 문서별 검토 결과 +6. 문서별 수정 내역 요약 +7. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +8. 업데이트된 파일 경로 목록 +9. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/release-confirm.prompt.md b/.github/prompts/release-confirm.prompt.md new file mode 100644 index 0000000..4a7b786 --- /dev/null +++ b/.github/prompts/release-confirm.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Use when: /release-confirm to validate release document readiness and mark it as confirmed from a Verification ID" +name: "release-confirm" +argument-hint: "Enter a Verification ID in ver-001 format." +agent: "agent" +model: "releaser" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Release 승인 처리 도우미다. +사용자가 입력한 Verification 문서 ID를 기준으로 연결된 Release 문서를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `ver-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Release 파일 탐색 규칙: + - 1차: ver-001 -> docs/sdlc/release/rel-001_*.release.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/release/*.release.md에서 `입력 Verification:` 줄에 해당 ver ID 또는 경로가 포함된 문서 검색 + - 최종 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Verification 검증 규칙: + - Release 문서의 `입력 Verification:` 줄에서 연결 Verification 문서 경로를 확인한다. + - 연결 Verification 문서가 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Verification 문서 상태가 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. Release 개요`에 입력 Verification/배포 대상 범위/배포 방식/배포 환경이 존재한다. + - `# 2. 배포 체크리스트`의 필수 6개 항목 결과가 모두 `완료` 또는 `해당없음`이다. + - `# 3. 롤백 계획`에 롤백 트리거/절차/데이터 롤백 전략/롤백 후 검증/롤백 커뮤니케이션이 모두 존재한다. + - `# 4. 릴리즈 노트 초안`에 변경 요약/주요 변경 내용/영향 범위/알려진 제한 사항이 존재한다. + - `# 5. 배포 후 관찰 계획`에 모니터링 항목 1개 이상과 관찰 기간이 존재한다. + - `# 6. 승인된 예외 인계 목록`이 존재한다(예외 없음은 "해당 없음" 허용). + - `# 7. Operations 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Operations 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 `##` 하위 항목은 모두 제거한다. +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Release 파일 탐색 +3. 문서 본문 읽기 +4. 연결 Verification 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/release-draft.prompt.md b/.github/prompts/release-draft.prompt.md new file mode 100644 index 0000000..9dd81b5 --- /dev/null +++ b/.github/prompts/release-draft.prompt.md @@ -0,0 +1,85 @@ +--- +description: "Use when: /release-draft to generate release draft, rollback plan, and operations handoff from a Verification document ID" +name: "release-draft" +argument-hint: "Enter a Verification ID in ver-001 format." +agent: "agent" +model: "releaser" +tools: [read, edit, search, agent, todo] +--- + +당신은 SDLC Release 초안 작성 도우미다. +사용자가 입력한 Verification 문서 ID를 기준으로 Release 초안(배포 체크리스트 + 롤백 계획 + 릴리즈 노트 + 관찰 계획)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `ver-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Verification 파일 검색 규칙: + - 경로: docs/sdlc/verification/ + - 패턴: <입력ID>_*.verification.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Verification 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Verification이 아직 승인되지 않았다. /verification-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Release 파일 생성 규칙: + - 저장 폴더: docs/sdlc/release/ + - 파일명: rel-<3자리문서번호>_YYYY-MM-DD_.release.md + - 문서번호: 기존 rel 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Release 본문 기본 템플릿: .github/templates/release-note.template.md +6. Release 작성 원칙: + - Verification의 품질 게이트 판정, 결함/예외, 회귀 결과를 근거로 배포 가능성 문맥을 작성한다. + - 배포 체크리스트(#2)는 항목별 결과(완료/미완료/해당없음)와 근거를 남긴다. + - 롤백 계획(#3)은 트리거/절차/데이터 롤백/검증/커뮤니케이션을 모두 포함한다. + - 릴리즈 노트(#4)는 변경 요약/영향 범위/알려진 제한 사항을 포함한다. + - 관찰 계획(#5)은 모니터링 지표/임계치/담당자를 포함한다. + - 승인된 예외 인계(#6)는 Verification 예외와 연결해 기록한다(없으면 해당 없음). +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Release의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +9. Operations 전달 체크 규칙: + - 롤백 가능한 배포 계획과 배포 체크리스트 근거가 있어야 한다. + - 관찰 포인트/장애 트리거/후속 대응 절차가 정의되어야 한다. + - 승인된 예외가 있으면 인계 이력이 포함되어야 한다. +10. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Verification 파일 검색 및 상태 확인 +3. Verification 본문(품질 게이트, 결함/예외, 회귀 결과, Release 전달 체크) 읽기 +4. 배포 체크리스트/롤백 계획/릴리즈 노트/관찰 계획 작성 +5. Release 문서 저장 +6. 본문 플레이스홀더 집계 후 # 9 작성 +7. 최종 응답에 처리 요약 제공 + +Release 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Release 개요 +2. 배포 체크리스트 +3. 롤백 계획 +4. 릴리즈 노트 초안 +5. 배포 후 관찰 계획 +6. 승인된 예외 인계 목록 +7. Operations 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Verification 문서 경로 +3. 형식 오류/미발견/미승인 여부 +4. Release 문서 생성/갱신 결과 +5. 추가된 사용자 결정 항목 요약 +6. 업데이트된 파일 경로 목록 +7. 다음 액션 + +사용자 입력: +{{input}} \ No newline at end of file diff --git a/.github/prompts/release-review.prompt.md b/.github/prompts/release-review.prompt.md new file mode 100644 index 0000000..9bab238 --- /dev/null +++ b/.github/prompts/release-review.prompt.md @@ -0,0 +1,80 @@ +--- +description: "Use when: /release-review to re-review existing release documents and update them in place from Verification IDs" +name: "release-review" +argument-hint: "Enter Verification IDs separated by commas. Example: ver-001, ver-002" +agent: "agent" +model: "releaser" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Release 문서 재검토 도우미다. +사용자가 지정한 Verification 문서 ID 목록을 기준으로 해당 Release 문서를 재검토하고, 필요한 보정을 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 Verification ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(ver-001 형식) + - 중복 ID 제거 +4. ID 형식은 `ver-<3자리숫자>`만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. Release 파일 탐색 규칙: + - 1차: ver-001 -> docs/sdlc/release/rel-001_*.release.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/release/*.release.md에서 `입력 Verification:` 줄에 해당 ver ID 또는 경로가 포함된 문서 검색 +6. 존재하지 않는 Release 문서는 생성하지 않는다. + - 미발견 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. +7. 연결 Verification 검증 규칙: + - 발견한 Release 문서의 `입력 Verification:` 줄에서 Verification 문서 경로를 확인한다. + - 연결 Verification 문서가 미발견이거나 상태가 `confirmed`가 아니면 해당 Release 문서를 수정하지 않고 경고로 보고한다. +8. 문서 재검토 항목: + - 섹션 0~9 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드 존재 여부 검증 + - 필드 누락 시 플레이스홀더로 보정(상태 불명확 -> `draft`, 승인 시각 미기재 -> `{{CONFIRM: 승인 후 기재}}`) + - `# 1. Release 개요`에 입력 Verification/배포 대상 범위/배포 방식/배포 환경 존재 여부 검증 + - `# 2. 배포 체크리스트`에 6개 필수 항목(설정/데이터/호환성/의존성/트래픽/커뮤니케이션) 존재 여부 검증 + - `# 3. 롤백 계획`에 트리거/절차/데이터 롤백/검증/커뮤니케이션/참조 런북 존재 여부 검증 + - `# 4. 릴리즈 노트 초안`에 변경 요약/주요 변경/영향 범위/제한 사항 존재 여부 검증 + - `# 5. 배포 후 관찰 계획`에 모니터링 표 1개 이상과 관찰 기간 존재 여부 검증 + - `# 6. 승인된 예외 인계 목록` 존재 여부 검증(예외 없음은 `해당 없음` 허용) + - `# 7. Operations 단계 전달 체크`의 체크 항목 수/문구/근거 일치성 검증 + - `# 8. 파일 처리 결과` 경로가 현재 문서 경로와 일치하는지 검증 +9. 체크 정규화 규칙: + - `# 7. Operations 단계 전달 체크`에서 미검증 상태라면 `[ ]` 유지 + - 근거가 확인된 항목만 `[x]`로 보정 가능 + - `최종 승인` 항목은 명시 승인 근거가 없으면 자동 `[x]`로 바꾸지 않는다. +10. 플레이스홀더 규칙: + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} +11. 플레이스홀더 집계 규칙: + - 본문의 미해결 플레이스홀더를 `# 9. 사용자 결정 필요 항목 요약`에 유형별 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. +12. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/근거를 유지하면서 누락/불일치/중복만 보정 +13. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. + +실행 절차: +1. 입력 ID 목록 정규화 및 형식 검증 +2. 각 ID에 대해 Release 문서 탐색(1차/2차 규칙 적용) +3. 발견 문서의 연결 Verification 존재/상태 확인 +4. 발견 문서 내용 읽기 +5. 재검토 항목 기준으로 누락/불일치 판단 +6. 필요한 항목만 보정하여 문서 overwrite 저장 +7. 문서별 플레이스홀더를 #9에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 Verification ID 목록 +3. 형식 오류 ID 목록(있으면) +4. Release 미발견 ID 목록(있으면) +5. 연결 Verification 미승인/미발견 목록(있으면) +6. 문서별 검토 결과 +7. 문서별 수정 내역 요약 +8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +9. 업데이트된 파일 경로 목록 +10. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/verification-confirm.prompt.md b/.github/prompts/verification-confirm.prompt.md new file mode 100644 index 0000000..76aec10 --- /dev/null +++ b/.github/prompts/verification-confirm.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Use when: /verification-confirm to validate verification document readiness and mark it as confirmed" +name: "verification-confirm" +argument-hint: "Enter a Verification ID in ver-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Verification 승인 처리 도우미다. +사용자가 입력한 Verification 문서 ID를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `ver-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/verification/ + - 패턴: <입력ID>_*.verification.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Implementation 검증 규칙: + - Verification 문서의 `입력 Implementation:` 줄에서 연결된 Implementation 문서 경로를 확인한다. + - 연결 Implementation 문서가 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Implementation 문서 상태가 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. Verification 개요`에 입력 Implementation/목적/검증 대상 작업이 존재한다. + - `# 2. 검증 범위`에 In Scope/Out of Scope가 모두 존재한다. + - `# 3. AC별 검증 결과`에 1개 이상의 검증 항목이 존재한다. + - `# 4. 결함 목록`과 심각도 기준이 존재한다. + - `# 5. 회귀 테스트 결과`에 1개 이상의 테스트 항목이 존재한다. + - `# 6. 예외 승인 목록`이 존재한다(예외 없음은 "해당 없음" 허용). + - `# 7. Release 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Release 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 섹션 헤더(`##`)와 항목은 모두 제거한다. +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Verification 파일 검색 +3. 문서 본문 읽기 +4. 연결 Implementation 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/verification-draft.prompt.md b/.github/prompts/verification-draft.prompt.md new file mode 100644 index 0000000..25eaea8 --- /dev/null +++ b/.github/prompts/verification-draft.prompt.md @@ -0,0 +1,84 @@ +--- +description: "Use when: /verification-draft to generate verification draft, quality-gate checks, and defect tracking plan from an Implementation document ID" +name: "verification-draft" +argument-hint: "Enter an Implementation ID in imp-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, agent, todo] +--- + +당신은 SDLC Verification 초안 작성 도우미다. +사용자가 입력한 Implementation 문서 ID를 기준으로 Verification 초안(AC 검증 결과 구조 + 결함 분류 + 품질 게이트 판정 + Release 전달 준비)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `imp-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Implementation 파일 검색 규칙: + - 경로: docs/sdlc/implementation/ + - 패턴: <입력ID>_*.implementation.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Implementation 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Implementation이 아직 승인되지 않았다. /implementation-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Verification 파일 생성 규칙: + - 저장 폴더: docs/sdlc/verification/ + - 파일명: ver-<3자리문서번호>_YYYY-MM-DD_.verification.md + - 문서번호: 기존 ver 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Verification 본문 기본 템플릿: .github/templates/verification-note.template.md +6. Verification 작성 원칙: + - Implementation의 작업 단위/검증 계획(#3, #5)을 기준으로 AC별 검증 결과 틀을 만든다. + - 결함 분류는 Blocker/Major/Minor 기준을 유지한다. + - 품질 게이트 판정(Blocker 0, Major 0 또는 승인 예외)을 명시한다. + - Release로 전달할 배포 가능 여부/예외 목록/잔여 리스크를 포함한다. +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Verification의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +9. Release 전달 체크 규칙: + - Blocker/Major 및 예외 승인 상태가 점검 가능해야 한다. + - 테스트 실행 근거가 문서에 연결되어야 한다. + - Release 전달 항목(배포 가능 여부, 잔여 리스크, 승인 예외)이 정리되어야 한다. +10. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Implementation 파일 검색 및 상태 확인 +3. Implementation 본문(작업 단위, 검증 계획, 영향도) 읽기 +4. AC별 검증 결과/결함/회귀/예외 구조 작성 +5. Release 전달 체크 항목 작성 +6. Verification 문서 저장 +7. 본문 플레이스홀더 집계 후 # 9 작성 +8. 최종 응답에 처리 요약 제공 + +Verification 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Verification 개요 +2. 검증 범위 +3. AC별 검증 결과 +4. 결함 목록 +5. 회귀 테스트 결과 +6. 예외 승인 목록 +7. Release 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Implementation 문서 경로 +3. 형식 오류/미발견/미승인 여부 +4. Verification 문서 생성/갱신 결과 +5. 추가된 사용자 결정 항목 요약 +6. 업데이트된 파일 경로 목록 +7. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/verification-review.prompt.md b/.github/prompts/verification-review.prompt.md new file mode 100644 index 0000000..d7c8acd --- /dev/null +++ b/.github/prompts/verification-review.prompt.md @@ -0,0 +1,83 @@ +--- +description: "Use when: /verification-review to re-review existing verification documents and update them in place from Verification IDs" +name: "verification-review" +argument-hint: "Enter Verification IDs separated by commas. Example: ver-001, ver-002" +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Verification 문서 재검토 도우미다. +사용자가 지정한 Verification 문서 ID 목록을 기준으로 해당 Verification 문서를 재검토하고, 필요한 보정을 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 Verification ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(ver-001 형식) + - 중복 ID 제거 +4. ID 형식은 `ver-<3자리숫자>`만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. Verification 파일 검색 규칙: + - 경로: `docs/sdlc/verification/` + - 패턴: `<입력ID>_*.verification.md` + - 미발견 ID는 미발견 목록으로 보고하고, 존재 문서만 재검토한다. +6. 존재하지 않는 Verification 문서는 생성하지 않는다. +7. 연결 Implementation 검증 규칙: + - 발견한 Verification 문서의 `입력 Implementation:` 줄에서 연결 경로를 확인한다. + - 연결된 Implementation 문서가 미발견이거나 상태가 `confirmed`가 아니면 해당 Verification 문서를 수정하지 않고 경고로 보고한다. +8. 문서 재검토 항목: + - 섹션 0~9 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드 존재 여부 검증 + - 상태가 누락/비정상 값이면 `draft`로 보정 + - 승인자 누락 시 `{{DECIDE: 이 Verification의 최종 승인자는 누구인가?}}`로 보정 + - 승인 시각 누락 시 `{{CONFIRM: 승인 후 기재}}`로 보정 + - `# 1. Verification 개요`에 입력 Implementation/목적/검증 대상 작업 존재 여부 검증 + - `# 2. 검증 범위`에 In Scope/Out of Scope 존재 여부 검증 + - `# 3. AC별 검증 결과`에 1개 이상의 검증 항목과 품질 게이트 판정 존재 여부 검증 + - `# 4. 결함 목록`과 심각도 기준 존재 여부 검증 + - `# 5. 회귀 테스트 결과`와 회귀 리스크 평가 존재 여부 검증 + - `# 6. 예외 승인 목록` 존재 여부 검증(예외 없음은 `해당 없음` 허용) + - `# 7. Release 단계 전달 체크`의 체크 항목 수/문구/근거 일치성 검증 + - `# 8. 파일 처리 결과`의 경로가 현재 문서 경로와 일치하는지 검증 +9. 체크 정규화 규칙: + - `# 7. Release 단계 전달 체크`에서 미검증 상태라면 `[ ]` 유지 + - 품질 게이트와 근거가 충족된 항목은 `[x]`로 보정 가능 + - `최종 승인` 항목은 명시 승인 근거가 없으면 자동 `[x]`로 바꾸지 않는다. +10. 플레이스홀더 규칙: + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - 추가 의사결정/확인/데이터가 필요한 경우 본문 관련 위치에 삽입한다. +11. 플레이스홀더 집계 규칙: + - 본문의 미해결 플레이스홀더를 `# 9. 사용자 결정 필요 항목 요약`에 유형별 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. +12. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/근거를 유지하면서 누락/불일치/중복만 보정 +13. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. + +실행 절차: +1. 입력 ID 목록 정규화 및 형식 검증 +2. 각 ID에 대해 Verification 문서 탐색 +3. 발견 문서의 연결 Implementation 존재/상태 확인 +4. 발견 문서 내용 읽기 +5. 재검토 항목 기준으로 누락/불일치 판단 +6. 필요한 항목만 보정하여 문서 overwrite 저장 +7. 문서별 플레이스홀더를 #9에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 Verification ID 목록 +3. 형식 오류 ID 목록(있으면) +4. Verification 미발견 ID 목록(있으면) +5. 연결 Implementation 미승인/미발견 목록(있으면) +6. 문서별 검토 결과 +7. 문서별 수정 내역 요약 +8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +9. 업데이트된 파일 경로 목록 +10. 다음 액션 + +사용자 입력: +{{input}} \ No newline at end of file diff --git a/.github/runbooks/services/api.rollback-runbook.md b/.github/runbooks/services/api.rollback-runbook.md new file mode 100644 index 0000000..39c8856 --- /dev/null +++ b/.github/runbooks/services/api.rollback-runbook.md @@ -0,0 +1,60 @@ +# API Service Rollback Runbook + +## Metadata + +1. Service: api +2. Runtime: uvicorn direct run +3. Environment: production +4. Owner: backend team +5. On-call Contact: backend-oncall +6. Last Updated (UTC): 2026-04-14 + +## Trigger Conditions + +1. Error Rate Threshold: 5xx > 2% for 5 minutes +2. Latency Threshold: p95 > 1200ms for 10 minutes +3. Availability Threshold: successful request rate < 99.5% +4. Data Integrity Alarm: write/read mismatch alerts > 0 +5. Business Impact Trigger: 결제/주문 API 실패 다수 발생 + +## Pre-rollback Checks + +1. 장애 범위 확인(엔드포인트/사용자/데이터) +2. 최근 릴리즈 커밋과 변경 DB 스크립트 확인 +3. 직전 안정 배포 아티팩트 식별 +4. 공지 채널(내부/외부) 준비 + +## Rollback Procedure + +1. 신규 배포 트래픽 중단 +2. 안정 버전 코드로 교체 +3. uvicorn 프로세스 재기동 +4. 헬스체크와 핵심 API 호출 검증 + +## Data Recovery Plan + +1. 복원 대상 데이터 범위: 최근 배포 이후 변경분 +2. 복원 방법: 백업 스냅샷 + 트랜잭션 로그 검증 +3. 데이터 무결성 검증 방법: 주문/결제 집계 대조 +4. 복원 예상 시간: 20~40분 + +## Validation After Rollback + +1. /health, /ready 정상 +2. 로그인/주문/결제 정상 시나리오 통과 +3. 5xx 비율 0.5% 이하 복귀 +4. p95 지연 700ms 이하 복귀 + +## Communication Plan + +1. 내부: 롤백 시작/완료 시점 공유 +2. 외부: 사용자 영향 공지(필요 시) +3. 상태 업데이트 주기: 15분 +4. 종료 공지 조건: 핵심 지표 30분 안정화 + +## Post-rollback Follow-up + +1. 원인 분석 티켓: INC-API-YYYYMMDD +2. 재배포 조건: 회귀 테스트 + 성능 검증 통과 +3. 임시 완화책 제거 계획: 48시간 내 정리 +4. Postmortem 일정: 24시간 내 \ No newline at end of file diff --git a/.github/runbooks/services/web.rollback-runbook.md b/.github/runbooks/services/web.rollback-runbook.md new file mode 100644 index 0000000..afabb64 --- /dev/null +++ b/.github/runbooks/services/web.rollback-runbook.md @@ -0,0 +1,58 @@ +# Web Service Rollback Runbook + +## Metadata + +1. Service: web +2. Environment: production +3. Owner: frontend team +4. On-call Contact: frontend-oncall +5. Last Updated (UTC): 2026-04-14 + +## Trigger Conditions + +1. JS Error Threshold: 신규 릴리즈 후 오류율 2배 이상 +2. Page Load Threshold: p75 LCP > 4.0s for 10 minutes +3. Availability Threshold: 주요 페이지 성공률 < 99.0% +4. Business Impact Trigger: 로그인/결제 전환율 급락 + +## Pre-rollback Checks + +1. 영향 페이지 및 브라우저 범위 확인 +2. CDN 캐시 정책 확인 +3. 직전 안정 빌드 해시 확인 +4. 공지 채널 준비 + +## Rollback Procedure + +1. CDN 라우팅을 이전 빌드로 전환 +2. 신규 정적 자산 캐시 무효화 +3. 핵심 사용자 플로우 스모크 테스트 실행 +4. 오류/성능 지표 확인 + +## Data Recovery Plan + +1. 복원 대상 데이터 범위: 클라이언트 이벤트 손실 구간 +2. 복원 방법: 서버 로그 기반 보정 집계 +3. 데이터 무결성 검증 방법: 퍼널 단계별 이벤트 대조 +4. 복원 예상 시간: 15~30분 + +## Validation After Rollback + +1. 로그인/탐색/결제 플로우 정상 +2. JS 오류율 기준치 복귀 +3. LCP 및 API 오류율 정상화 +4. 사용자 CS 급증 여부 확인 + +## Communication Plan + +1. 내부: 배포 채널 및 온콜 채널 공지 +2. 외부: 장애 공지 페이지 업데이트(필요 시) +3. 상태 업데이트 주기: 15분 +4. 종료 공지 조건: 30분 이상 안정화 + +## Post-rollback Follow-up + +1. 원인 분석 티켓: INC-WEB-YYYYMMDD +2. 재배포 조건: 크로스브라우저 회귀 테스트 통과 +3. 임시 완화책 제거 계획: 48시간 내 +4. Postmortem 일정: 24시간 내 \ No newline at end of file diff --git a/.github/runbooks/services/worker.rollback-runbook.md b/.github/runbooks/services/worker.rollback-runbook.md new file mode 100644 index 0000000..590db8c --- /dev/null +++ b/.github/runbooks/services/worker.rollback-runbook.md @@ -0,0 +1,58 @@ +# Worker Service Rollback Runbook + +## Metadata + +1. Service: worker +2. Environment: production +3. Owner: platform team +4. On-call Contact: platform-oncall +5. Last Updated (UTC): 2026-04-14 + +## Trigger Conditions + +1. Queue Backlog Threshold: 대기 작업 10,000건 초과 10분 지속 +2. Failure Threshold: 작업 실패율 > 3% for 10 minutes +3. Processing Delay Threshold: 평균 처리 지연 > 2x baseline +4. Data Integrity Alarm: 중복 처리 또는 누락 처리 감지 +5. Business Impact Trigger: 정산/알림 지연 사용자 영향 확대 + +## Pre-rollback Checks + +1. 실패 작업 샘플링 및 영향 범위 확인 +2. 재처리 가능 여부 확인 +3. 직전 안정 버전 아티팩트 확인 +4. 공지 채널 준비 + +## Rollback Procedure + +1. 신규 작업 소비 일시 중단 +2. 안정 버전으로 워커 교체 +3. 단계적 소비 재개(10% -> 50% -> 100%) +4. 실패/처리율 지표 확인 + +## Data Recovery Plan + +1. 복원 대상 데이터 범위: 누락/중복 처리된 이벤트 +2. 복원 방법: dead-letter queue 재처리 + idempotency 검증 +3. 데이터 무결성 검증 방법: 이벤트 카운트/체크섬 대조 +4. 복원 예상 시간: 30~60분 + +## Validation After Rollback + +1. backlog 감소 추세 확인 +2. 실패율 1% 이하 복귀 +3. 중복 처리 경보 해제 +4. 핵심 배치 완료 시간 정상화 + +## Communication Plan + +1. 내부: 대응 진행 상황 15분 주기 공유 +2. 외부: SLA 영향 시 사전 공지 +3. 종료 공지 조건: backlog 정상화 및 실패율 안정화 + +## Post-rollback Follow-up + +1. 원인 분석 티켓: INC-WORKER-YYYYMMDD +2. 재배포 조건: 부하 테스트 재통과 +3. 임시 완화책 제거 계획: 72시간 내 +4. Postmortem 일정: 24시간 내 \ No newline at end of file diff --git a/.github/scripts/bootstrap.sh b/.github/scripts/bootstrap.sh new file mode 100644 index 0000000..11c0ee0 --- /dev/null +++ b/.github/scripts/bootstrap.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +log() { + printf '[bootstrap] %s\n' "$*" +} + +fail() { + printf '[bootstrap] ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_linux() { + if [ "$(uname -s)" != "Linux" ]; then + fail "This bootstrap script supports Linux only." + fi +} + +ensure_repo_structure() { + local dirs=( + ".github/agents" + ".github/instructions" + ".github/prompts" + ".github/runbooks/services" + ".github/scripts" + ".github/templates" + "docs/sdlc/discovery" + "docs/sdlc/planning" + "docs/sdlc/design" + "docs/sdlc/verification" + "docs/sdlc/release" + "docs/sdlc/operations" + ) + for dir in "${dirs[@]}"; do + mkdir -p "${REPO_ROOT}/${dir}" + done +} + +report_github_structure() { + log "Current .github structure:" + if command -v find >/dev/null 2>&1; then + find "${REPO_ROOT}/.github" -maxdepth 2 -mindepth 1 | sed "s#${REPO_ROOT}/##" | sort + else + log "find command not available; skipping structure scan." + fi +} + +has_python_311_or_higher() { + command -v python3 >/dev/null 2>&1 \ + && python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' +} + +install_python_311() { + if has_python_311_or_higher; then + return 0 + fi + if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then + log "Installing Python 3.11 via apt-get (minimum required: 3.11)." + sudo apt-get update + sudo apt-get install -y python3.11 + else + fail "Python 3.11+ is required. Install it manually or provide apt-get + sudo." + fi + has_python_311_or_higher || fail "Python 3.11+ installation check failed." +} + +has_node_20() { + command -v node >/dev/null 2>&1 \ + && node -e 'const [major] = process.versions.node.split(".").map(Number); process.exit(major >= 20 ? 0 : 1);' +} + +install_node_20() { + if has_node_20; then + return 0 + fi + if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then + log "Installing Node.js 20 LTS via NodeSource (minimum required: v20)." + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + else + fail "Node.js v20+ is required. Install it manually or provide apt-get + sudo + curl." + fi + has_node_20 || fail "Node.js v20+ installation check failed." +} + +main() { + require_linux + log "Repository root: ${REPO_ROOT}" + ensure_repo_structure + install_python_311 + install_node_20 + report_github_structure + log "python3: $(python3 --version 2>&1)" + log "node: $(node --version 2>&1)" + log "Bootstrap completed successfully." +} + +main "$@" \ No newline at end of file diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md new file mode 100644 index 0000000..f23db62 --- /dev/null +++ b/.github/skills/release-notes/SKILL.md @@ -0,0 +1,26 @@ +--- +name: release-notes +description: "Use when: release note drafting, deployment communication, change impact summary" +--- + +# Release Notes Skill + +## Goal + +배포 변경사항과 사용자 영향을 이해하기 쉬운 형식으로 전달한다. + +## Workflow + +1. 변경사항을 사용자 영향 기준으로 분류한다. +2. 주의사항/호환성/알려진 이슈를 정리한다. +3. 롤백 또는 대응 가이드를 포함한다. +4. 배포 후 모니터링 포인트를 함께 제공한다. + +## Output Template + +1. Summary +2. What Changed +3. User Impact +4. Migration or Compatibility Notes +5. Known Issues and Workarounds +6. Rollback and Monitoring Notes diff --git a/.github/skills/requirements-refinement/SKILL.md b/.github/skills/requirements-refinement/SKILL.md new file mode 100644 index 0000000..8fdb1d7 --- /dev/null +++ b/.github/skills/requirements-refinement/SKILL.md @@ -0,0 +1,26 @@ +--- +name: requirements-refinement +description: "Use when: vague request, requirement clarification, acceptance criteria drafting" +--- + +# Requirements Refinement Skill + +## Goal + +모호한 요청을 실행 가능한 요구사항과 수용 기준으로 변환한다. + +## Workflow + +1. 문제/목표/제약을 분리해 질문 목록을 만든다. +2. 기능/비기능 요구사항을 구조화한다. +3. 각 요구사항에 수용 기준(AC)을 붙인다. +4. 범위 밖 항목(Out of Scope)을 명시한다. + +## Output Template + +1. Problem Statement +2. Functional Requirements +3. Non-Functional Requirements +4. Acceptance Criteria +5. Out of Scope +6. Open Questions diff --git a/.github/skills/test-design/SKILL.md b/.github/skills/test-design/SKILL.md new file mode 100644 index 0000000..2de4ee3 --- /dev/null +++ b/.github/skills/test-design/SKILL.md @@ -0,0 +1,26 @@ +--- +name: test-design +description: "Use when: test case design, verification planning, regression risk assessment" +--- + +# Test Design Skill + +## Goal + +요구사항과 변경 영향도 기반으로 누락 없는 테스트 시나리오를 만든다. + +## Workflow + +1. 수용 기준을 테스트 관점으로 분해한다. +2. 정상/경계/예외 시나리오를 도출한다. +3. 회귀 영향 영역을 식별한다. +4. 우선순위 기반 실행 순서를 제안한다. + +## Output Template + +1. Test Scope +2. Positive Cases +3. Boundary Cases +4. Negative Cases +5. Regression Targets +6. Execution Priority diff --git a/.github/templates/design-note.template.md b/.github/templates/design-note.template.md new file mode 100644 index 0000000..0aa50f7 --- /dev/null +++ b/.github/templates/design-note.template.md @@ -0,0 +1,71 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Design의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Design 개요 +- 입력 Planning: +- 목적: +- 설계 대상 작업 (WSJF 상위 3개): + - 1순위: + - 2순위: + - 3순위: + +# 2. 설계 대안 비교 (우선순위 상위 작업) +## <작업 ID>: <작업명> +### 대안 A +- 설명: +- 장점: +- 단점: + +### 대안 B +- 설명: +- 장점: +- 단점: + +### 선택 결정 +- 선택: 대안 A / 대안 B +- 근거: + +# 3. 인터페이스 정의 +## <작업 ID>: <작업명> +- 입력: +- 출력: +- 오류 경로: + +# 4. 데이터/흐름 설계 +## <작업 ID>: <작업명> +- 주요 데이터 경로: +- 상태 변화: + +# 5. 테스트 전략 연계 (AC 매핑) +| 작업 ID | AC | 검증 방법 | 검증 포인트 | +|---|---|---|---| +| P-001 | AC-1 | | | +| P-001 | AC-2 | | | + +# 6. 리스크 대응 설계 +| 리스크 ID | 리스크 내용 | 설계 대응 전략 | +|---|---|---| +| R-1 | | | + +# 7. Implementation 단계 전달 체크 +- [ ] 우선순위 상위 작업의 설계 근거(대안 비교 + 선택 이유)가 존재함 +- [ ] 전체 작업의 인터페이스(입력/출력/오류 경로)가 정의됨 +- [ ] AC와 검증 포인트가 매핑됨 +- [ ] Planning 리스크에 대한 설계 대응이 반영됨 +- [ ] 최종 승인 + - {{CONFIRM: Design 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Design 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/discovery-note.template.md b/.github/templates/discovery-note.template.md new file mode 100644 index 0000000..795a106 --- /dev/null +++ b/.github/templates/discovery-note.template.md @@ -0,0 +1,90 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 승인자를 지정하라}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. 계획 상태 요약 +- Discovery 문서 현황: +- Planning 점수 파일 현황: docs/sdlc/planning/planning-backlog-scores.out.csv +- 해석: + +# 2. 요구사항 판정 결과 (완전 중복/유사/신규 + 근거) +- 판정: {{DECIDE: 완전 중복/유사/신규 중 하나를 선택하라}} +- 근거 + - + - + +# 3. 문제점의 요약 +- 현재 상태 + - +- 기대 상태 + - +- 이해관계자 + - +- 제약사항 + - + +# 4. 이번에 만들 기능 1개 +- + +# 5. 요구사항 목록 +## 기능 요구사항 +- FR-1: +- FR-2: + +## 비기능 요구사항 +- NFR-1: +- NFR-2: + +## 범위 경계 +- In Scope + - +- Out of Scope + - + +# 6. 리스크/가정 목록 +## 리스크 +- R-1 (High/Medium/Low): + - 완화 방안: +- R-2 (High/Medium/Low): + - 완화 방안: + +## 가정 +- A-1: +- A-2: + +# 7. 초기 성공 기준 +- S-1: +- S-2: + +## 측정 방식 및 데이터 출처 +- S-1 측정 방식: / 데이터 출처: +- S-2 측정 방식: / 데이터 출처: + +# 8. Planning으로 넘기기 전 확인 체크 +- Planning 시작 시점은 {{DECIDE: 즉시 가능/보류 중 선택하라}}. +- [ ] 범위(In Scope/Out of Scope) 확정 + - 기준: +- [ ] 성공 지표의 측정 방식/데이터 출처 확정 + - 기준: +- [ ] High 영향 플레이스홀더 우선순위 및 해소 책임자 지정 + - 기준: +- [ ] 승인자 확정 + - 기준: + +# 9. 파일 처리 결과 (생성/미생성 경로) +- 처리 결과: +- 생성 경로: +- 재검토: +- 참조 문서: + +# 10. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- (없으면 이 섹션 제거) + +## {{DATA}} +- (없으면 이 섹션 제거) + diff --git a/.github/templates/implementation-note.template.md b/.github/templates/implementation-note.template.md new file mode 100644 index 0000000..32110d9 --- /dev/null +++ b/.github/templates/implementation-note.template.md @@ -0,0 +1,62 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Implementation의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Implementation 개요 +- 입력 Design: +- 목적: +- 이번 구현 우선순위 작업: + - 1순위: + - 2순위: + - 3순위: + +# 2. 이번 구현 범위 +## In Scope +- + +## Out of Scope +- + +# 3. 작업 단위 분해 +| Impl ID | 작업명 | 연결 설계/AC | 변경 유형 | 우선순위 | +|---|---|---|---|---| +| I-001 | | | | | + +# 4. 파일 변경 계획 +| Impl ID | 대상 파일 | 변경 목적 | 변경 종류(생성/수정) | +|---|---|---|---| +| I-001 | | | | + +# 5. 테스트/검증 계획 +| Impl ID | 검증 방법 | 성공 기준 | 근거 | +|---|---|---|---| +| I-001 | | | | + +# 6. 영향도 및 의존성 점검 +| 항목 | 영향 내용 | 대응 방안 | +|---|---|---| +| 코드 | | | +| 문서 | | | +| 설정/의존성 | | | + +# 7. Verification 단계 전달 체크 +- [ ] 구현 범위가 설계 문서와 연결됨 +- [ ] 작업 단위별 검증 방법이 정의됨 +- [ ] 영향 파일과 의존성 점검 결과가 기록됨 +- [ ] Verification에 전달할 실행/검증 근거가 정의됨 +- [ ] 최종 승인 + - {{CONFIRM: Implementation 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Implementation 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/planning-backlog-scores.template.csv b/.github/templates/planning-backlog-scores.template.csv new file mode 100644 index 0000000..07b685a --- /dev/null +++ b/.github/templates/planning-backlog-scores.template.csv @@ -0,0 +1,4 @@ +item_id,work_item,business_value,time_criticality,rr_oe,job_size,reach,impact,confidence,effort,method,notes +P-001,Example work item A,8,7,6,5,1000,2,80,8,WSJF,baseline sample +P-002,Example work item B,6,8,5,3,3000,1,70,10,RICE,marketing-facing change +P-003,Example work item C,9,6,8,8,500,3,90,5,WSJF,risk reduction heavy diff --git a/.github/templates/planning-note.template.md b/.github/templates/planning-note.template.md new file mode 100644 index 0000000..c33b453 --- /dev/null +++ b/.github/templates/planning-note.template.md @@ -0,0 +1,63 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Planning의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Planning 개요 +- 입력 Discovery: +- 목적: +- 우선순위 방법: WSJF +- 점수 스케일: Business Value/Time Criticality/RR-OE/Job Size 모두 1~10 + +# 2. 이번 반복 범위 경계 +## In Scope +- + +## Out of Scope +- + +# 3. 백로그 작업 분해 +| Item ID | Work Item | 연결 요구사항 | 연결 리스크 | 담당 | +|---|---|---|---|---| +| P-001 | | | | | + +# 4. WSJF 점수표 +| Item ID | Work Item | Business Value | Time Criticality | RR/OE | Job Size | WSJF Score | Priority | +|---|---:|---:|---:|---:|---:|---:|---:| +| P-001 | | | | | | | | + +계산식: WSJF = (Business Value + Time Criticality + RR/OE) / Job Size + +## 점수 근거 메모 +- P-001: + +# 5. 작업별 수용 기준 (AC) +## P-001 AC +- AC-1: +- AC-2: + +# 6. 우선순위 조정/예외 +- 동률 처리: +- Override: +- 승인자: + +# 7. Design 단계 전달 체크 +- [ ] 우선순위 상위 백로그의 AC가 검증 가능한 문장으로 정의됨 +- [ ] WSJF 계산 근거 기록 완료 +- [ ] 이번 반복 In Scope/Out of Scope 확정 +- [ ] 최종 승인 + - {{CONFIRM: Planning 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: +- 점수 파일 갱신: docs/sdlc/planning/planning-backlog-scores.out.csv + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Planning 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/planning-scoring-sheet.template.csv b/.github/templates/planning-scoring-sheet.template.csv new file mode 100644 index 0000000..7dbd2be --- /dev/null +++ b/.github/templates/planning-scoring-sheet.template.csv @@ -0,0 +1,4 @@ +item_id,work_item,business_value,time_criticality,rr_oe,job_size,reach,impact,confidence,effort,method,wsjf_score,rice_score,selected_score,priority +P-001,Example work item A,8,7,6,5,1000,2,80,8,WSJF,=(C2+D2+E2)/F2,=(G2*H2*I2)/J2,=IF(K2="WSJF",L2,M2),= +P-002,Example work item B,6,8,5,3,3000,1,70,10,RICE,=(C3+D3+E3)/F3,=(G3*H3*I3)/J3,=IF(K3="WSJF",L3,M3),= +P-003,Example work item C,9,6,8,8,500,3,90,5,WSJF,=(C4+D4+E4)/F4,=(G4*H4*I4)/J4,=IF(K4="WSJF",L4,M4),= diff --git a/.github/templates/release-note.template.md b/.github/templates/release-note.template.md new file mode 100644 index 0000000..4a6410d --- /dev/null +++ b/.github/templates/release-note.template.md @@ -0,0 +1,105 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Release의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Release 개요 +- 입력 Verification: +- 배포 대상 범위: +- 배포 방식: {{DECIDE: 점진 배포/일괄 배포 중 선택하라}} +- 배포 환경: + +# 2. 배포 체크리스트 + +| 항목 | 확인 내용 | 결과 (완료/미완료/해당없음) | 비고 | +|---|---|---|---| +| 설정 검증 | 환경 변수, 시크릿, 외부 연동 키 유효성 확인 | | | +| 데이터 검증 | 마이그레이션 순서, 백업 상태, 복구 가능성 확인 | | | +| 호환성 검증 | 이전 버전 클라이언트/API 호환 여부 확인 | | | +| 의존성 검증 | 신규/변경 라이브러리 버전 충돌 여부 확인 | | | +| 트래픽 전략 | 점진/일괄 배포 방식과 중단 기준 확인 | | | +| 커뮤니케이션 | 사용자 공지, 내부 공지, 온콜 담당자 공유 완료 | | | + +# 3. 롤백 계획 + +## 롤백 트리거 +- 에러율 임계치: +- 지연 임계치: +- 장애 지표: + +## 롤백 절차 +1. +2. +3. + +## 데이터 롤백 전략 +- 스키마/데이터 복원 여부: +- 절차: + +## 롤백 후 검증 +- 핵심 시나리오 확인 항목: + - + +## 롤백 커뮤니케이션 +- 공지 채널: +- 공지 시각: +- 상태 업데이트 주기: + +## 참조 런북 +- 롤백 런북: .github/templates/rollback-runbook.template.md + +# 4. 릴리즈 노트 초안 + +## 변경 요약 +- + +## 주요 변경 내용 +- + +## 영향 범위 +- 사용자 영향: +- 시스템 영향: + +## 알려진 제한 사항 +- + +# 5. 배포 후 관찰 계획 + +| 모니터링 항목 | 측정 지표 | 정상 범위 | 이상 감지 기준 | 담당자 | +|---|---|---|---|---| +| | | | | | + +## 관찰 기간 +- 집중 관찰 기간: +- 일반 관찰 기간: + +# 6. 승인된 예외 인계 목록 + +| 예외 ID | 출처 (Verification) | 심각도 | 완화책 | 유효 기간 (UTC) | Operations 후속 조치 | +|---|---|---|---|---|---| +| | | | | | | + +> 예외 없음 시 이 섹션을 "해당 없음"으로 처리한다. + +# 7. Operations 단계 전달 체크 +- [ ] 롤백 가능한 배포 계획 승인됨 +- [ ] 배포 체크리스트 항목 전체 "완료" 또는 "해당없음" 처리됨 +- [ ] 릴리즈/롤백 체크리스트가 근거와 함께 완료됨 +- [ ] 배포 후 관찰 항목이 정의됨 +- [ ] Operations에 전달할 관찰 포인트, 장애 트리거, 후속 대응 절차 준비됨 +- [ ] 승인된 예외 이력 인계됨 +- [ ] 최종 승인 + - {{CONFIRM: Release 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Release 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/rollback-runbook.template.md b/.github/templates/rollback-runbook.template.md new file mode 100644 index 0000000..d8fceb5 --- /dev/null +++ b/.github/templates/rollback-runbook.template.md @@ -0,0 +1,62 @@ +# Rollback Runbook Template + +## Metadata + +1. Change ID: +2. Release Version: +3. Environment: +4. Owner: +5. On-call Contact: +6. Last Updated (UTC): + +## Trigger Conditions + +1. Error Rate Threshold: +2. Latency Threshold: +3. Availability Threshold: +4. Data Integrity Alarm: +5. Business Impact Trigger: + +## Pre-rollback Checks + +1. 장애 범위 확인(기능/사용자/데이터) +2. 최근 배포 변경점 확인 +3. 백업/스냅샷 가용성 확인 +4. 커뮤니케이션 채널 준비 + +## Rollback Procedure + +1. 트래픽 차단 또는 점진 축소 +2. 이전 안정 버전으로 아티팩트 전환 +3. 마이그레이션 롤백 또는 안전 모드 전환 +4. 캐시/큐/비동기 작업 상태 정리 +5. 서비스 재기동 및 헬스체크 확인 + +## Data Recovery Plan + +1. 복원 대상 데이터 범위: +2. 복원 방법(백업/스냅샷/리플레이): +3. 데이터 무결성 검증 방법: +4. 복원 예상 시간: + +## Validation After Rollback + +1. 핵심 사용자 시나리오 통과 여부 +2. API 오류율 정상화 여부 +3. 지연 시간 정상화 여부 +4. 데이터 정합성 점검 결과 +5. 모니터링 알림 해소 여부 + +## Communication Plan + +1. 롤백 선언 메시지(내부) +2. 영향 공지 메시지(외부) +3. 상태 업데이트 주기 +4. 종료 공지 조건 + +## Post-rollback Follow-up + +1. 원인 분석 티켓: +2. 재배포 조건: +3. 임시 완화책 제거 계획: +4. Postmortem 일정: diff --git a/.github/templates/verification-note.template.md b/.github/templates/verification-note.template.md new file mode 100644 index 0000000..fd73a54 --- /dev/null +++ b/.github/templates/verification-note.template.md @@ -0,0 +1,83 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Verification의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Verification 개요 +- 입력 Implementation: +- 목적: +- 검증 대상 작업: + - 1순위: + - 2순위: + - 3순위: + +# 2. 검증 범위 +## In Scope +- + +## Out of Scope +- + +# 3. AC별 검증 결과 + +| Impl ID | AC | 검증 방법 | 결과 (통과/실패/보류) | 비고 | +|---|---|---|---|---| +| I-001 | | | | | + +## 품질 게이트 판정 +- Blocker 결함 수: +- Major 결함 수: +- 승인된 예외 건수: +- 배포 가능 여부: {{DECIDE: 배포 가능/불가 중 선택하라}} + +# 4. 결함 목록 + +| 결함 ID | 연결 Impl ID | 심각도 (Blocker/Major/Minor) | 증상 | 재현 여부 | 처리 상태 | +|---|---|---|---|---|---| +| D-001 | | | | | | + +## 심각도 기준 +- Blocker: 서비스 중단, 데이터 손실/오염, 보안 취약점 악용 가능 상태 +- Major: 핵심 기능 오동작, 우회 가능하지만 사용자 영향이 큰 결함 +- Minor: 비핵심 기능 이슈, 문구/표시 오류, 영향이 제한적인 결함 + +# 5. 회귀 테스트 결과 + +| 테스트 항목 | 영향 영역 | 실행 결과 | 실패 시 원인 | +|---|---|---|---| +| | | | | + +## 회귀 리스크 평가 +- 영향 영역: +- 롤백 조건: + +# 6. 예외 승인 목록 + +| 예외 ID | 연결 결함 | 심각도 | 완화책 | 승인자 | 승인 시각 (UTC) | 유효 기간 (UTC) | +|---|---|---|---|---|---|---| +| | | | | | | | + +> 예외 없음 시 이 섹션을 "해당 없음"으로 처리한다. + +# 7. Release 단계 전달 체크 +- [ ] Blocker 결함 0건 확인 +- [ ] Major 결함 0건 또는 승인된 예외 존재 +- [ ] 변경 코드 경로의 테스트 실행 증거(로그/리포트) 첨부 +- [ ] 신규 기능의 정상 경로 테스트 1개 이상 및 예외 경로 테스트 1개 이상 확인 +- [ ] 회귀 테스트 실패 없음 또는 원인 분석/롤백 계획 존재 +- [ ] Release에 전달할 배포 가능 여부, 예외 승인 정보, 잔여 리스크 준비됨 +- [ ] 최종 승인 + - {{CONFIRM: Verification 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Verification 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/tools/calc_priority_scores.sh b/.github/tools/calc_priority_scores.sh new file mode 100755 index 0000000..0268819 --- /dev/null +++ b/.github/tools/calc_priority_scores.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [output_csv]" + exit 1 +fi + +infile="$1" +outfile="${2:-}" + +tmp_scored="$(mktemp)" +tmp_sorted="$(mktemp)" +trap 'rm -f "$tmp_scored" "$tmp_sorted"' EXIT + +awk -F',' 'BEGIN { OFS="," } +NR==1 { + print "item_id","work_item","method","wsjf_score","rice_score","selected_score","notes" + next +} +{ + item=$1 + work=$2 + bv=$3+0 + tc=$4+0 + rroe=$5+0 + js=$6+0 + reach=$7+0 + impact=$8+0 + conf=$9+0 + effort=$10+0 + method=toupper($11) + notes=$12 + + wsjf=(js==0)?0:((bv+tc+rroe)/js) + rice=(effort==0)?0:((reach*impact*conf)/effort) + selected=(method=="RICE")?rice:wsjf + + printf "%s,%s,%s,%.4f,%.4f,%.4f,%s\n", item, work, method, wsjf, rice, selected, notes +} +' "$infile" > "$tmp_scored" + +{ + head -n 1 "$tmp_scored" + tail -n +2 "$tmp_scored" | sort -t',' -k6,6gr +} > "$tmp_sorted" + +awk -F',' 'BEGIN { OFS="," } +NR==1 { + print $0, "priority" + next +} +{ + print $0, NR-1 +} +' "$tmp_sorted" > "${outfile:-/dev/stdout}" diff --git a/bootstrap/bootstrap.sh b/bootstrap/bootstrap.sh new file mode 100644 index 0000000..c63f251 --- /dev/null +++ b/bootstrap/bootstrap.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +set -euo pipefail + +log() { + printf '[bootstrap] %s\n' "$*" +} + +fail() { + printf '[bootstrap] ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_linux() { + if [ "$(uname -s)" != "Linux" ]; then + fail "This bootstrap script supports Linux only." + fi +} + +HOST_ROOT="${1:-$(pwd)}" + +ensure_host_structure() { + local dirs=( + ".github" + ".github/agents" + ".github/instructions" + ".github/prompts" + ".github/runbooks/services" + ".github/scripts" + ".github/templates" + "docs/sdlc/discovery" + "docs/sdlc/planning" + "docs/sdlc/design" + "docs/sdlc/implementation" + "docs/sdlc/verification" + "docs/sdlc/release" + "docs/sdlc/operations" + ) + + for dir in "${dirs[@]}"; do + mkdir -p "${HOST_ROOT}/${dir}" + done +} + +report_github_structure() { + log "Host .github structure:" + if command -v find >/dev/null 2>&1; then + find "${HOST_ROOT}/.github" -maxdepth 2 -mindepth 1 | sed "s#${HOST_ROOT}/##" | sort + else + log "find command not available; skipping structure scan." + fi +} + +has_python_311_or_higher() { + command -v python3 >/dev/null 2>&1 \ + && python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' +} + +install_python_311() { + if has_python_311_or_higher; then + return 0 + fi + if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then + log "Installing Python 3.11 via apt-get (minimum required: 3.11)." + sudo apt-get update + sudo apt-get install -y python3.11 + else + fail "Python 3.11+ is required. Install it manually or provide apt-get + sudo." + fi + has_python_311_or_higher || fail "Python 3.11+ installation check failed." +} + +has_node_20() { + command -v node >/dev/null 2>&1 \ + && node -e 'const [major] = process.versions.node.split(".").map(Number); process.exit(major >= 20 ? 0 : 1);' +} + +install_node_20() { + if has_node_20; then + return 0 + fi + if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then + log "Installing Node.js 20 LTS via NodeSource (minimum required: v20)." + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + else + fail "Node.js v20+ is required. Install it manually or provide apt-get + sudo + curl." + fi + has_node_20 || fail "Node.js v20+ installation check failed." +} + +main() { + require_linux + [ -d "${HOST_ROOT}" ] || fail "Host root not found: ${HOST_ROOT}" + + log "Host root: ${HOST_ROOT}" + ensure_host_structure + install_python_311 + install_node_20 + report_github_structure + log "python3: $(python3 --version 2>&1)" + log "node: $(node --version 2>&1)" + log "Bootstrap completed successfully." +} + +main "$@" diff --git a/bootstrap/install.sh b/bootstrap/install.sh new file mode 100644 index 0000000..f77f3bf --- /dev/null +++ b/bootstrap/install.sh @@ -0,0 +1,282 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: install.sh [--dry-run] [HOST_ROOT] [POLICY] + +Options: + --dry-run Show planned changes without modifying files. + --source-path + Source path for StagePilot assets: root/.github|base/.github + (default: root/.github) + --cleanup-legacy-base + Remove legacy base/.github directory under host root. + -h, --help Show this help message. + +Arguments: + HOST_ROOT Target host repository root (default: current directory) + POLICY Conflict policy for non-copilot files: replace|preserve|fail + (default: STAGEPILOT_CONFLICT_POLICY or preserve) +EOF +} + +log() { + printf '[install] %s\n' "$*" +} + +fail() { + printf '[install] ERROR: %s\n' "$*" >&2 + exit 1 +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +DRY_RUN=0 +SOURCE_PATH="root/.github" +CLEANUP_LEGACY_BASE=0 +ARGS=() +while [ "$#" -gt 0 ]; do + case "$1" in + --dry-run) + DRY_RUN=1 + ;; + --source-path) + [ "$#" -ge 2 ] || fail "--source-path requires a value" + SOURCE_PATH="$2" + shift + ;; + --cleanup-legacy-base) + CLEANUP_LEGACY_BASE=1 + ;; + -h|--help) + usage + exit 0 + ;; + --*) + fail "Unknown option: $1" + ;; + *) + ARGS+=("$1") + ;; + esac + shift +done + +HOST_ROOT="${ARGS[0]:-$(pwd)}" +POLICY="${ARGS[1]:-${STAGEPILOT_CONFLICT_POLICY:-preserve}}" +TARGET_GITHUB="${HOST_ROOT}/.github" +STAGEPILOT_BEGIN="" +STAGEPILOT_END="" + +case "${SOURCE_PATH}" in + root/.github) + SOURCE_GITHUB="${PACKAGE_ROOT}/.github" + ;; + base/.github) + SOURCE_GITHUB="${PACKAGE_ROOT}/base/.github" + ;; + *) + fail "Invalid source path '${SOURCE_PATH}'. Allowed: root/.github|base/.github" + ;; +esac + +ALLOWLIST_FILE="${PACKAGE_ROOT}/bootstrap/source-allowlist.txt" + +case "${POLICY}" in + replace|preserve|fail) + ;; + *) + fail "Invalid conflict policy '${POLICY}'. Allowed: replace|preserve|fail" + ;; +esac + +[ -d "${SOURCE_GITHUB}" ] || fail "Source package not found: ${SOURCE_GITHUB}" +[ -d "${HOST_ROOT}" ] || fail "Host root not found: ${HOST_ROOT}" +[ -f "${ALLOWLIST_FILE}" ] || fail "Allowlist not found: ${ALLOWLIST_FILE}" + +is_allowed_relpath() { + local rel_path="$1" + while IFS= read -r raw_line; do + line="${raw_line%%#*}" + line="${line%$'\r'}" + line="$(printf '%s' "${line}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" + [ -n "${line}" ] || continue + + if [[ "${line}" == */ ]]; then + [[ "${rel_path}" == "${line}"* ]] && return 0 + else + [[ "${rel_path}" == "${line}" ]] && return 0 + fi + done < "${ALLOWLIST_FILE}" + + return 1 +} + +merge_stagepilot_section() { + local src_file="$1" + local dst_file="$2" + local payload_file + local output_file + + payload_file="$(mktemp)" + output_file="$(mktemp)" + + # If source already has marker block, use only that block as payload. + if grep -Fq "${STAGEPILOT_BEGIN}" "${src_file}" && grep -Fq "${STAGEPILOT_END}" "${src_file}"; then + awk -v begin="${STAGEPILOT_BEGIN}" -v end="${STAGEPILOT_END}" ' + $0 == begin { in_block=1; next } + $0 == end { in_block=0; exit } + in_block { print } + ' "${src_file}" > "${payload_file}" + else + cat "${src_file}" > "${payload_file}" + fi + + if grep -Fq "${STAGEPILOT_BEGIN}" "${dst_file}" && grep -Fq "${STAGEPILOT_END}" "${dst_file}"; then + awk -v begin="${STAGEPILOT_BEGIN}" -v end="${STAGEPILOT_END}" -v payload="${payload_file}" ' + $0 == begin { + print + while ((getline line < payload) > 0) { + print line + } + in_block=1 + next + } + $0 == end { + in_block=0 + print + next + } + !in_block { print } + ' "${dst_file}" > "${output_file}" + else + cat "${dst_file}" > "${output_file}" + { + printf '\n' + printf '## StagePilot\n' + printf '%s\n' "${STAGEPILOT_BEGIN}" + cat "${payload_file}" + printf '%s\n' "${STAGEPILOT_END}" + } >> "${output_file}" + fi + + if [ "${DRY_RUN}" -eq 1 ]; then + log "[dry-run] merge preview: ${dst_file}" + if command -v diff >/dev/null 2>&1; then + diff -u "${dst_file}" "${output_file}" || true + else + log "[dry-run] diff command not available; preview skipped for ${dst_file}" + fi + else + cp -a "${output_file}" "${dst_file}" + fi + + rm -f "${payload_file}" "${output_file}" +} + +copied=0 +skipped=0 +conflicted=0 +failed=0 +merged=0 + +mkdir -p "${TARGET_GITHUB}" + +while IFS= read -r -d '' src_file; do + rel_path="${src_file#${SOURCE_GITHUB}/}" + + if ! is_allowed_relpath "${rel_path}"; then + skipped=$((skipped + 1)) + continue + fi + + dst_file="${TARGET_GITHUB}/${rel_path}" + dst_dir="$(dirname "${dst_file}")" + + mkdir -p "${dst_dir}" + + if [ ! -e "${dst_file}" ]; then + if [ "${DRY_RUN}" -eq 1 ]; then + log "[dry-run] copy: ${rel_path}" + else + cp -a "${src_file}" "${dst_file}" + fi + copied=$((copied + 1)) + continue + fi + + if cmp -s "${src_file}" "${dst_file}"; then + skipped=$((skipped + 1)) + continue + fi + + if [ "${rel_path}" = "copilot-instructions.md" ]; then + merge_stagepilot_section "${src_file}" "${dst_file}" + merged=$((merged + 1)) + continue + fi + + conflicted=$((conflicted + 1)) + + case "${POLICY}" in + replace) + if [ "${DRY_RUN}" -eq 1 ]; then + log "[dry-run] replace: ${rel_path}" + else + cp -a "${src_file}" "${dst_file}" + fi + copied=$((copied + 1)) + ;; + preserve) + skipped=$((skipped + 1)) + ;; + fail) + printf '[install] CONFLICT: %s\n' "${dst_file}" >&2 + failed=1 + ;; + esac + +done < <(find "${SOURCE_GITHUB}" -type f -print0 | sort -z) + +if [ "${CLEANUP_LEGACY_BASE}" -eq 1 ]; then + LEGACY_BASE_DIR="${HOST_ROOT}/base/.github" + if [ -d "${LEGACY_BASE_DIR}" ]; then + if [ "${DRY_RUN}" -eq 1 ]; then + log "[dry-run] delete legacy path: ${LEGACY_BASE_DIR}" + else + rm -rf "${LEGACY_BASE_DIR}" || fail "LEGACY_BASE_DELETE_ERROR: failed to remove ${LEGACY_BASE_DIR}" + log "deleted legacy path: ${LEGACY_BASE_DIR}" + fi + else + log "legacy path not found, skip cleanup: ${LEGACY_BASE_DIR}" + fi +fi + +log "host_root=${HOST_ROOT}" +log "source=${SOURCE_GITHUB}" +log "source_path=${SOURCE_PATH}" +log "allowlist=${ALLOWLIST_FILE}" +log "cleanup_legacy_base=${CLEANUP_LEGACY_BASE}" +log "policy=${POLICY}" +log "copied=${copied} skipped=${skipped} conflicted=${conflicted} merged=${merged}" + +if [ "${failed}" -ne 0 ]; then + if [ "${DRY_RUN}" -eq 1 ]; then + fail "[dry-run] Conflicts would occur with policy=fail." + fi + fail "Conflicts encountered with policy=fail. No bootstrap run." +fi + +if [ "${DRY_RUN}" -eq 1 ]; then + log "[dry-run] Bootstrap step skipped. No files were changed." + exit 0 +fi + +BOOTSTRAP_SCRIPT="${PACKAGE_ROOT}/bootstrap/bootstrap.sh" +[ -f "${BOOTSTRAP_SCRIPT}" ] || fail "Bootstrap script not found: ${BOOTSTRAP_SCRIPT}" + +/bin/bash "${BOOTSTRAP_SCRIPT}" "${HOST_ROOT}" +log "Install completed successfully." diff --git a/bootstrap/source-allowlist.txt b/bootstrap/source-allowlist.txt new file mode 100644 index 0000000..905262c --- /dev/null +++ b/bootstrap/source-allowlist.txt @@ -0,0 +1,14 @@ +# Relative paths from selected source .github directory. +# Files +AGENTS.md +START-HERE.md +copilot-instructions.md + +# Directories +agents/ +instructions/ +prompts/ +scripts/ +skills/ +templates/ +tools/ From edf7e61a44464b1426a2367bb825239651c51907 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 22:02:39 +0900 Subject: [PATCH 09/21] feat: add update script and operations docs/templates --- .github/START-HERE.md | 12 +- .github/prompts/operation-confirm.prompt.md | 4 +- .github/prompts/operation-draft.prompt.md | 7 +- .github/prompts/operation-review.prompt.md | 2 +- .github/templates/operations-note.template.md | 80 ++++++++++ README.md | 97 ++++++++++++ bootstrap/bootstrap.sh | 68 +++------ bootstrap/update.sh | 144 ++++++++++++++++++ 8 files changed, 359 insertions(+), 55 deletions(-) create mode 100644 .github/templates/operations-note.template.md create mode 100644 README.md create mode 100755 bootstrap/update.sh diff --git a/.github/START-HERE.md b/.github/START-HERE.md index 8f44c52..118f035 100644 --- a/.github/START-HERE.md +++ b/.github/START-HERE.md @@ -38,13 +38,21 @@ bash .github/tools/calc_priority_scores.sh .github/templates/planning-backlog-sc - .github/runbooks/services/web.rollback-runbook.md 3. 배포 전 각 서비스 인스턴스의 Trigger/Procedure/Validation 최신화 -## 4. Verification Exception Quick Start +## 4. Operations Quick Start + +1. 기본 템플릿: .github/templates/operations-note.template.md +2. 생성 프롬프트: /operation-draft rel-001 +3. 검토 프롬프트: /operation-review rel-001 +4. 승인 프롬프트: /operation-confirm rel-001 +5. 산출 경로: docs/sdlc/operations/ops-<3자리문서번호>_YYYY-MM-DD_.operations.md + +## 5. Verification Exception Quick Start 1. Major 결함 예외는 승인자 정책을 따름 2. Valid Until 설정은 만료 전 재검증 필수 3. 만료/해소 상태를 검증 리포트에 반영 -## 5. Minimum Go-live Checklist +## 6. Minimum Go-live Checklist 1. 우선순위 점수표 완료 2. 테스트 실행 근거 첨부 diff --git a/.github/prompts/operation-confirm.prompt.md b/.github/prompts/operation-confirm.prompt.md index 6b65afc..13ebeac 100644 --- a/.github/prompts/operation-confirm.prompt.md +++ b/.github/prompts/operation-confirm.prompt.md @@ -33,8 +33,8 @@ tools: [read, edit, search, todo] 7. 승인 가능 시 아래 순서로 문서를 수정한다: a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. - c. `# 7. 파일 처리 결과` 항목에 Operations 완료 시각과 마감 상태를 추가한다. - d. `# 8. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 `##` 하위 항목은 모두 제거한다. + c. `# 6. 파일 처리 결과` 항목에 Operations 완료 시각과 마감 상태를 추가한다. + d. `# 7. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 `##` 하위 항목은 모두 제거한다. 8. 환류 항목 처리 규칙: - `# 5. 다음 반복 환류 항목`에 명시된 각 개선 항목에 대해 다음 Discovery/Planning 백로그 연결을 확인한다. - 자동으로 연결하지 않으며, 사용자가 수동 등록한 것만 인계 완료로 표시한다. diff --git a/.github/prompts/operation-draft.prompt.md b/.github/prompts/operation-draft.prompt.md index 2a1f48c..4df4543 100644 --- a/.github/prompts/operation-draft.prompt.md +++ b/.github/prompts/operation-draft.prompt.md @@ -24,6 +24,7 @@ tools: [read, edit, search, agent, todo] - 파일명: ops-<3자리문서번호>_YYYY-MM-DD_.operations.md - 문서번호: 기존 ops 번호 최대값 + 1, 없으면 001부터 시작 - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - 본문 템플릿: .github/templates/operations-note.template.md - append 금지, 생성/갱신 시 overwrite 6. Operations 작성 원칙: - Release의 배포 체크리스트, 롤백 계획, 관찰 계획을 근거로 운영 추적 문맥을 작성한다. @@ -42,7 +43,7 @@ tools: [read, edit, search, agent, todo] - 상태: 초안 생성 시 `draft` - 승인자: Release 문서에서 인계받음 - 승인 시각: `{{CONFIRM: 배포 완료 후 Operations 확인}}` -9. # 6 사용자 결정 필요 항목 요약 규칙: +9. # 7 사용자 결정 필요 항목 요약 규칙: - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. @@ -57,7 +58,7 @@ tools: [read, edit, search, agent, todo] 8. Postmortem 섹션 생성 (8개 항목 모두 {{DATA:}} 또는 등급별 예시 포함) 9. 환류 항목 섹션 생성 (발견된 개선 사항 추출 또는 {{DATA:}}) 10. Operations 문서 저장 -11. 본문 플레이스홀더 집계 후 # 6 작성 +11. 본문 플레이스홀더 집계 후 # 7 작성 12. 최종 응답에 처리 요약 제공 Operations 문서 출력 섹션 순서(고정): @@ -75,7 +76,7 @@ Operations 문서 출력 섹션 순서(고정): 2. 입력 Release 문서 경로 3. Operations 문서 생성/갱신 결과 4. 배포 실행 기록 초안 요약 -5. 모니터링 관찰 항목 복사 상여 +5. 모니터링 관찰 항목 복사 상세 6. Postmortem 템플릿 완성도 7. 업데이트된 파일 경로 목록 8. 다음 액션 (배포 실행자가 DATA 항목 채우기) diff --git a/.github/prompts/operation-review.prompt.md b/.github/prompts/operation-review.prompt.md index 331713d..1741fd5 100644 --- a/.github/prompts/operation-review.prompt.md +++ b/.github/prompts/operation-review.prompt.md @@ -30,7 +30,7 @@ tools: [read, edit, search, todo] - 본문에 `{{DATA:` 플레이스홀더가 남아 있으면 미완성으로 표시한다. 6. 정규화 규칙: - {{DATA:}} 플레이스홀더는 유지하면서, 사용자가 실제 데이터를 작성한 항목은 플레이스홀더 제거 - - 모든 섹션이 완성되면 # 7 플레이스홀더를 정리한다 + - 모든 섹션이 완성되면 # 7 사용자 결정 필요 항목 요약 정리를 안내한다 - 배포 실행 결과나 Postmortem이 일부 만료되지 않았다면, 진행 중(미완료) 상태 명시 후 진행 7. 응답 규칙: - 검토 결과를 미충족 항목 목록으로 보고한다 (별도 수정 없음) diff --git a/.github/templates/operations-note.template.md b/.github/templates/operations-note.template.md new file mode 100644 index 0000000..bace579 --- /dev/null +++ b/.github/templates/operations-note.template.md @@ -0,0 +1,80 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Operations의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 배포 완료 후 Operations 확인}} + +# 1. 배포 실행 기록 +- 입력 Release: {{DATA: docs/sdlc/release/rel-XXX_YYYY-MM-DD_topic.release.md}} +- 배포 방식: {{DATA: 점진 배포/일괄 배포}} +- 배포 환경: {{DATA: production/staging 등}} +- 배포 창(UTC): {{DATA: 시작/종료 시각}} + +## 사전 검증 +- 체크리스트 점검 결과: {{DATA: 완료/미완료 항목}} +- 롤백 준비 상태: {{DATA: 준비됨/미준비 + 근거}} + +## 실행 결과 +- 실행 명령/절차 요약: {{DATA: 실행 로그 요약}} +- 결과: {{DATA: 성공/부분성공/실패}} +- 영향 범위: {{DATA: 사용자/시스템 영향}} + +## 배포 직후 검증 결과 +- 핵심 시나리오 검증: {{DATA: 통과/실패 + 근거}} +- 예외/알림 발생 여부: {{DATA: 있음/없음 + 내용}} + +# 2. 모니터링 관찰 결과 +| 모니터링 항목 | 측정 지표 | 정상 범위 | 이상 감지 기준 | 관찰 결과 | 비고 | +|---|---|---|---|---|---| +| {{DATA: 항목명}} | {{DATA: 지표}} | {{DATA: 정상 범위}} | {{DATA: 임계치}} | {{DATA: 결과}} | | + +## 관찰 기간 +- 집중 관찰 기간: {{DATA: 시작~종료}} +- 일반 관찰 기간: {{DATA: 시작~종료}} + +# 3. 인시던트 대응 기록 +- 인시던트 발생 여부: {{DATA: 발생/해당없음}} + +## 발생한 경우 기록 +- 영향도 분류: {{DATA: 사용자/기능/데이터 영향}} +- 타임라인: + - 탐지 시각(UTC): {{DATA: 시각}} + - 대응 시작(UTC): {{DATA: 시각}} + - 완화 시각(UTC): {{DATA: 시각}} + - 복구 완료(UTC): {{DATA: 시각}} +- 커뮤니케이션: + - 채널: {{DATA: 채널}} + - 공지 시각(UTC): {{DATA: 시각}} + - 업데이트 주기: {{DATA: 주기}} +- 우회책 실행 여부: {{DATA: 실행/미실행 + 내용}} +- 복구 절차 요약: {{DATA: 복구 단계}} + +# 4. Postmortem +- What Happened: {{DATA: 사건 개요와 사용자 관찰 증상}} +- Impact: {{DATA: 서비스/사용자/비즈니스 영향}} +- Root Cause: {{DATA: 기술적 원인 + 시스템적 원인}} +- Detection: {{DATA: 탐지 경로/지연 요인/알림 품질}} +- Response: {{DATA: 대응 의사결정 및 실행 평가}} +- Recovery: {{DATA: 복구 절차와 검증 결과}} +- Preventive Actions: {{DATA: 재발 방지 액션(담당자/기한 포함)}} +- Follow-up Tracking: {{DATA: Discovery/Planning 백로그 연결 상태}} + +# 5. 다음 반복 환류 항목 +| 환류 ID | 개선 항목 | 연결 대상(Discovery/Planning) | 담당자 | 목표일 | 상태 | +|---|---|---|---|---|---| +| FB-001 | {{DATA: 개선 항목}} | {{DATA: dcy/pln 문서 또는 티켓}} | {{DATA: 담당자}} | {{DATA: YYYY-MM-DD}} | {{DATA: 등록됨/대기}} | + +# 6. 파일 처리 결과 +- 생성/갱신 경로: {{DATA: docs/sdlc/operations/ops-XXX_YYYY-MM-DD_topic.operations.md}} +- 입력 Release 경로: {{DATA: docs/sdlc/release/rel-XXX_YYYY-MM-DD_topic.release.md}} +- 처리 결과: {{DATA: 생성/갱신/검토}} +- 비고: {{DATA: 특이사항}} + +# 7. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- (없으면 이 섹션 제거) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/README.md b/README.md new file mode 100644 index 0000000..bddfc3d --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ + +# 사용방법 + +## 최초 등록 + +```bash +# 리모트 등록 +git remote add stage-pilot https://github.com/genichin/stage-pilot.git +# subtree 추가 +git fetch stage-pilot main +git subtree add --prefix=.vendor/stage-pilot stage-pilot main --squash +# 설치 +bash .vendor/stage-pilot/bootstrap/install.sh . +``` + +## 업데이트 + +```bash +# 원격이 등록된 경우(stage-pilot remote 우선 사용) +bash .vendor/stage-pilot/bootstrap/update.sh . + +# 원격 없이 URL로 직접 업데이트 +bash .vendor/stage-pilot/bootstrap/update.sh --repo-url https://github.com/genichin/stage-pilot.git . + +# 설치 재적용 없이 subtree만 갱신 +bash .vendor/stage-pilot/bootstrap/update.sh --skip-install . +``` + +## SDLC 프롬프트 사용법 + +`.github/prompts/`에 단계별 프롬프트가 정의되어 있으며, Copilot Chat에서 `/프롬프트이름 인자` 형태로 실행한다. + +### 기본 실행 순서 + +1. Discovery +2. Planning +3. Design +4. Implementation +5. Verification +6. Release +7. Operations + +### 단계별 명령 예시 + +```text +# Discovery +/discovery-draft 결제 실패 알림 정책을 도입한다 +/discovery-review dcy-001 +/discovery-confirm dcy-001 + +# Planning +/planning-draft dcy-001 +/planning-review dcy-001 +/planning-confirm pln-001 + +# Design +/design-draft pln-001 +/design-review dsn-001 +/design-confirm dsn-001 + +# Implementation +/implementation-draft dsn-001 +/implementation imp-001 I-001, I-002 +/implementation-confirm imp-001 + +# Verification +/verification-draft imp-001 +/verification-review ver-001 +/verification-confirm ver-001 + +# Release +/release-draft ver-001 +/release-review ver-001 +/release-confirm ver-001 + +# Operations +/operation-draft rel-001 +/operation-review rel-001 +/operation-confirm rel-001 +``` + +### 문서 ID 규칙 + +1. Discovery: `dcy-001` +2. Planning: `pln-001` +3. Design: `dsn-001` +4. Implementation: `imp-001` +5. Verification: `ver-001` +6. Release: `rel-001` + +### 참고 파일 + +1. 시작 가이드: `.github/START-HERE.md` +2. 단계 규칙: `.github/instructions/` +3. 프롬프트 정의: `.github/prompts/` +4. 템플릿: `.github/templates/` +5. Operations 템플릿: `.github/templates/operations-note.template.md` diff --git a/bootstrap/bootstrap.sh b/bootstrap/bootstrap.sh index c63f251..c42b43f 100644 --- a/bootstrap/bootstrap.sh +++ b/bootstrap/bootstrap.sh @@ -11,10 +11,8 @@ fail() { exit 1 } -require_linux() { - if [ "$(uname -s)" != "Linux" ]; then - fail "This bootstrap script supports Linux only." - fi +current_os() { + uname -s } HOST_ROOT="${1:-$(pwd)}" @@ -51,55 +49,31 @@ report_github_structure() { fi } -has_python_311_or_higher() { - command -v python3 >/dev/null 2>&1 \ - && python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' -} - -install_python_311() { - if has_python_311_or_higher; then - return 0 - fi - if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then - log "Installing Python 3.11 via apt-get (minimum required: 3.11)." - sudo apt-get update - sudo apt-get install -y python3.11 - else - fail "Python 3.11+ is required. Install it manually or provide apt-get + sudo." - fi - has_python_311_or_higher || fail "Python 3.11+ installation check failed." -} - -has_node_20() { - command -v node >/dev/null 2>&1 \ - && node -e 'const [major] = process.versions.node.split(".").map(Number); process.exit(major >= 20 ? 0 : 1);' -} - -install_node_20() { - if has_node_20; then - return 0 - fi - if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then - log "Installing Node.js 20 LTS via NodeSource (minimum required: v20)." - curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - - sudo apt-get install -y nodejs - else - fail "Node.js v20+ is required. Install it manually or provide apt-get + sudo + curl." - fi - has_node_20 || fail "Node.js v20+ installation check failed." -} - main() { - require_linux + os_name="$(current_os)" [ -d "${HOST_ROOT}" ] || fail "Host root not found: ${HOST_ROOT}" log "Host root: ${HOST_ROOT}" ensure_host_structure - install_python_311 - install_node_20 + + # Intentionally do not install dependencies in bootstrap. + # This script only prepares the host directory layout. + log "${os_name} detected. Skipping Python/Node installation by design." + report_github_structure - log "python3: $(python3 --version 2>&1)" - log "node: $(node --version 2>&1)" + + if command -v python3 >/dev/null 2>&1; then + log "python3: $(python3 --version 2>&1)" + else + log "python3: not found" + fi + + if command -v node >/dev/null 2>&1; then + log "node: $(node --version 2>&1)" + else + log "node: not found" + fi + log "Bootstrap completed successfully." } diff --git a/bootstrap/update.sh b/bootstrap/update.sh new file mode 100755 index 0000000..43c0549 --- /dev/null +++ b/bootstrap/update.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: update.sh [options] [HOST_ROOT] + +Options: + --dry-run Show planned commands without executing them. + --remote Git remote name to use when available (default: stage-pilot) + --repo-url Fallback repository URL when remote is missing. + (default: https://github.com/genichin/stage-pilot.git) + --branch Upstream branch to sync (default: main) + --prefix Subtree prefix path (default: .vendor/stage-pilot) + --policy install.sh conflict policy: replace|preserve|fail + (default: preserve) + --skip-install Only update subtree. Do not run install.sh. + -h, --help Show this help message. + +Arguments: + HOST_ROOT Host repository root (default: current directory) +EOF +} + +log() { + printf '[update] %s\n' "$*" +} + +fail() { + printf '[update] ERROR: %s\n' "$*" >&2 + exit 1 +} + +run_cmd() { + if [ "${DRY_RUN}" -eq 1 ]; then + log "[dry-run] $*" + else + "$@" + fi +} + +DRY_RUN=0 +REMOTE_NAME="stage-pilot" +REPO_URL="https://github.com/genichin/stage-pilot.git" +BRANCH="main" +PREFIX=".vendor/stage-pilot" +POLICY="preserve" +SKIP_INSTALL=0 +ARGS=() + +while [ "$#" -gt 0 ]; do + case "$1" in + --dry-run) + DRY_RUN=1 + ;; + --remote) + [ "$#" -ge 2 ] || fail "--remote requires a value" + REMOTE_NAME="$2" + shift + ;; + --repo-url) + [ "$#" -ge 2 ] || fail "--repo-url requires a value" + REPO_URL="$2" + shift + ;; + --branch) + [ "$#" -ge 2 ] || fail "--branch requires a value" + BRANCH="$2" + shift + ;; + --prefix) + [ "$#" -ge 2 ] || fail "--prefix requires a value" + PREFIX="$2" + shift + ;; + --policy) + [ "$#" -ge 2 ] || fail "--policy requires a value" + POLICY="$2" + shift + ;; + --skip-install) + SKIP_INSTALL=1 + ;; + -h|--help) + usage + exit 0 + ;; + --*) + fail "Unknown option: $1" + ;; + *) + ARGS+=("$1") + ;; + esac + shift +done + +HOST_ROOT="${ARGS[0]:-$(pwd)}" + +case "${POLICY}" in + replace|preserve|fail) + ;; + *) + fail "Invalid --policy '${POLICY}'. Allowed: replace|preserve|fail" + ;; +esac + +[ -d "${HOST_ROOT}" ] || fail "Host root not found: ${HOST_ROOT}" + +if ! git -C "${HOST_ROOT}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + fail "Host root is not a git repository: ${HOST_ROOT}" +fi + +if git -C "${HOST_ROOT}" remote get-url "${REMOTE_NAME}" >/dev/null 2>&1; then + SOURCE_REF="${REMOTE_NAME}" + log "Using git remote '${REMOTE_NAME}'" +else + SOURCE_REF="${REPO_URL}" + log "Remote '${REMOTE_NAME}' not found; using URL '${REPO_URL}'" +fi + +if [ -d "${HOST_ROOT}/${PREFIX}" ]; then + run_cmd git -C "${HOST_ROOT}" subtree pull --prefix="${PREFIX}" "${SOURCE_REF}" "${BRANCH}" --squash +else + log "Subtree prefix not found; adding subtree at ${PREFIX}" + run_cmd git -C "${HOST_ROOT}" subtree add --prefix="${PREFIX}" "${SOURCE_REF}" "${BRANCH}" --squash +fi + +if [ "${SKIP_INSTALL}" -eq 1 ]; then + log "Skip install requested. Update completed." + exit 0 +fi + +INSTALL_SCRIPT="${HOST_ROOT}/${PREFIX}/bootstrap/install.sh" +[ -f "${INSTALL_SCRIPT}" ] || fail "Install script not found after update: ${INSTALL_SCRIPT}" + +if [ "${DRY_RUN}" -eq 1 ]; then + run_cmd /bin/bash "${INSTALL_SCRIPT}" --dry-run "${HOST_ROOT}" "${POLICY}" +else + /bin/bash "${INSTALL_SCRIPT}" "${HOST_ROOT}" "${POLICY}" +fi + +log "Update completed successfully." From 9caa073658946c83bb15dd443472a761e532fa01 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 22:06:39 +0900 Subject: [PATCH 10/21] draft --- .../implementation-from-approved-req.md | 72 ----- ...open-requirement-discovery.instructions.md | 50 ---- ...requirement-scope-decision.instructions.md | 52 ---- .github/instructions/srs.instructions.md | 53 ---- .github/prompts/approve-requirement.prompt.md | 26 -- .../prompts/complete-requirement.prompt.md | 32 --- .../create-implementation-issue.prompt.md | 42 --- .../decide-requirement-scope.prompt.md | 38 --- .../mark-requirement-implemented.prompt.md | 25 -- .../reopen-requirement-status.prompt.md | 44 --- .github/prompts/sync-srs-index.prompt.md | 28 -- .../prompts/sync-technical-specs.prompt.md | 34 --- .../prompts/validate-requirement.prompt.md | 24 -- .github/pull_request_template.md | 24 -- .vendor/myagent-kit/README.md | 152 ---------- .../implementation-from-approved-req.md | 72 ----- .../base/.github/copilot-instructions.md | 16 -- ...open-requirement-discovery.instructions.md | 50 ---- ...requirement-scope-decision.instructions.md | 52 ---- .../.github/instructions/srs.instructions.md | 53 ---- .../prompts/approve-requirement.prompt.md | 26 -- .../prompts/complete-requirement.prompt.md | 32 --- .../create-implementation-issue.prompt.md | 42 --- .../decide-requirement-scope.prompt.md | 38 --- .../mark-requirement-implemented.prompt.md | 25 -- .../reopen-requirement-status.prompt.md | 44 --- .../.github/prompts/sync-srs-index.prompt.md | 28 -- .../prompts/sync-technical-specs.prompt.md | 34 --- .../prompts/validate-requirement.prompt.md | 24 -- .../base/.github/pull_request_template.md | 24 -- .vendor/myagent-kit/bootstrap/install.sh | 263 ------------------ .../templates/docs/SRS/REQ-template.md | 55 ---- .../docs/SRS/TRACEABILITY-template.md | 28 -- .../implementation-from-approved-req.md | 72 ----- ...requirement-scope-decision.instructions.md | 52 ---- base/.github/instructions/srs.instructions.md | 53 ---- .../prompts/approve-requirement.prompt.md | 26 -- .../prompts/complete-requirement.prompt.md | 32 --- .../create-implementation-issue.prompt.md | 42 --- .../decide-requirement-scope.prompt.md | 38 --- .../mark-requirement-implemented.prompt.md | 25 -- base/.github/prompts/sync-srs-index.prompt.md | 28 -- .../prompts/validate-requirement.prompt.md | 24 -- base/.github/pull_request_template.md | 24 -- bootstrap/install.sh | 19 -- 45 files changed, 2037 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/implementation-from-approved-req.md delete mode 100644 .github/instructions/reopen-requirement-discovery.instructions.md delete mode 100644 .github/instructions/requirement-scope-decision.instructions.md delete mode 100644 .github/instructions/srs.instructions.md delete mode 100644 .github/prompts/approve-requirement.prompt.md delete mode 100644 .github/prompts/complete-requirement.prompt.md delete mode 100644 .github/prompts/create-implementation-issue.prompt.md delete mode 100644 .github/prompts/decide-requirement-scope.prompt.md delete mode 100644 .github/prompts/mark-requirement-implemented.prompt.md delete mode 100644 .github/prompts/reopen-requirement-status.prompt.md delete mode 100644 .github/prompts/sync-srs-index.prompt.md delete mode 100644 .github/prompts/sync-technical-specs.prompt.md delete mode 100644 .github/prompts/validate-requirement.prompt.md delete mode 100644 .github/pull_request_template.md delete mode 100644 .vendor/myagent-kit/README.md delete mode 100644 .vendor/myagent-kit/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md delete mode 100644 .vendor/myagent-kit/base/.github/copilot-instructions.md delete mode 100644 .vendor/myagent-kit/base/.github/instructions/reopen-requirement-discovery.instructions.md delete mode 100644 .vendor/myagent-kit/base/.github/instructions/requirement-scope-decision.instructions.md delete mode 100644 .vendor/myagent-kit/base/.github/instructions/srs.instructions.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/approve-requirement.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/complete-requirement.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/create-implementation-issue.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/decide-requirement-scope.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/mark-requirement-implemented.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/reopen-requirement-status.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/sync-srs-index.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/sync-technical-specs.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/prompts/validate-requirement.prompt.md delete mode 100644 .vendor/myagent-kit/base/.github/pull_request_template.md delete mode 100755 .vendor/myagent-kit/bootstrap/install.sh delete mode 100644 .vendor/myagent-kit/templates/docs/SRS/REQ-template.md delete mode 100644 .vendor/myagent-kit/templates/docs/SRS/TRACEABILITY-template.md delete mode 100644 base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md delete mode 100644 base/.github/instructions/requirement-scope-decision.instructions.md delete mode 100644 base/.github/instructions/srs.instructions.md delete mode 100644 base/.github/prompts/approve-requirement.prompt.md delete mode 100644 base/.github/prompts/complete-requirement.prompt.md delete mode 100644 base/.github/prompts/create-implementation-issue.prompt.md delete mode 100644 base/.github/prompts/decide-requirement-scope.prompt.md delete mode 100644 base/.github/prompts/mark-requirement-implemented.prompt.md delete mode 100644 base/.github/prompts/sync-srs-index.prompt.md delete mode 100644 base/.github/prompts/validate-requirement.prompt.md delete mode 100644 base/.github/pull_request_template.md delete mode 100755 bootstrap/install.sh diff --git a/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md b/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md deleted file mode 100644 index 42e8832..0000000 --- a/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: "Implementation From Approved Requirement" -about: "Create an implementation issue from an Approved SRS requirement" -title: "[REQ-XXX] Implement " -labels: ["implementation", "requirement"] -assignees: [] ---- - - - -## 배경 (Context) -- Requirement ID: -- Requirement Link: -- Status Check: Approved - -## 범위 (Scope) -- In Scope: -- Out of Scope: - -## 구현 전 설계검토 체크리스트 (Pre-Implementation Design Review) -- [ ] Architecture approach is documented and agreed -- [ ] Public interface/API contract changes are identified -- [ ] Data model/schema impact is reviewed -- [ ] Error handling and fallback strategy is defined -- [ ] Security/privacy impact is reviewed -- [ ] Performance/scalability impact is reviewed -- [ ] Test strategy (unit/integration/e2e/manual) is defined -- [ ] Rollback or feature-flag strategy is defined when needed -- [ ] Unknowns, assumptions, and decision owners are recorded -- [ ] Human review sign-off completed (or `No additional design review required.`) - -### 설계 결정 요약 (Design Decisions) -- Decision 1: -- Decision 2: - -### 미해결 항목 / 가정 (Open Items / Assumptions) -- - -## AI Agent 작업 (AI Agent Work) -- [ ] Code change task 1 -- [ ] Code change task 2 -- [ ] Automated validation task - -## 사람 작업 (Human Work) -- [ ] Approve scope or open questions -- [ ] Perform required manual verification - -## Acceptance Criteria Mapping -- REQ AC1 -> Task(s): -- REQ AC2 -> Task(s): - -## AI 검증 요약 (AI Validation Summary) -- Automated tests/checks to run: -- Expected evidence: - -## 사람 확인 체크리스트 (Human Verification Checklist) -- [ ] Verify: -- [ ] Confirm: -- If no human verification is required, write: `No manual verification required.` - -## 열린 질문 (Open Questions) -- - -## 리스크 (Risks) -- - -## 완료 조건 (Definition of Done) -- [ ] All mapped acceptance criteria are satisfied -- [ ] Tests are added or updated -- [ ] AI-completable work is finished and evidence is attached -- [ ] Human-only work is completed or explicitly marked not required -- [ ] Docs or change notes are updated if needed diff --git a/.github/instructions/reopen-requirement-discovery.instructions.md b/.github/instructions/reopen-requirement-discovery.instructions.md deleted file mode 100644 index bb579df..0000000 --- a/.github/instructions/reopen-requirement-discovery.instructions.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -description: "Use when customizing REQ candidate discovery rules for /reopen-requirement-status command, including keyword dictionary, scoring, tie-breaks, and normalization policy." -applyTo: ".github/prompts/reopen-requirement-status.prompt.md" ---- -# Reopen Requirement Discovery Guidelines - -## 목적 -`/reopen-requirement-status` 입력에 REQ ID가 없을 때, 관련 REQ 후보를 일관된 기준으로 탐색하고 우선순위를 정한다. - -## 후보 탐색 우선순위 -1. 계약 키워드 일치 -- `API`, `endpoint`, `request`, `response`, `schema`, `field`, `CLI`, `command`, `option`, `UI`, `event`, `config`, `env`, `deploy`, `integration` -2. 영향 영역 일치 -- 입력의 변경 대상 모듈/문서/테스트와 REQ의 `Impacted Area` 교집합 -3. 목적/행동 일치 -- 입력의 동사/의도(추가, 제거, 변경, 완화, 강화, 비활성화 등)와 REQ `Intent`/`Requirement`의 의미 유사도 -4. 상태 가중치 -- 현재 상태가 `Approved`, `Implemented`, `Deprecated`인 REQ 우선 - -## 점수 및 신뢰도 -- 점수식: `키워드(0-5) + 영향영역(0-3) + 의미유사(0-2) + 상태가중치(0-1)` -- 정렬: 총점 내림차순 -- 신뢰도: - - `High(7-11)` - - `Medium(4-6)` - - `Low(0-3)` - -## 도메인 키워드 사전 (커스터마이즈 가능) -- Auth & Access: `auth`, `login`, `oauth`, `token`, `permission`, `rbac`, `role`, `scope` -- Billing & Subscription: `billing`, `invoice`, `payment`, `subscription`, `plan`, `quota`, `credit` -- Data & Migration: `schema`, `migration`, `backfill`, `etl`, `import`, `export`, `retention` -- API & Contract: `api`, `endpoint`, `request`, `response`, `versioning`, `webhook`, `payload` -- Runtime & Operations: `deploy`, `rollback`, `release`, `monitoring`, `alert`, `slo`, `incident` -- Configuration: `config`, `env`, `feature flag`, `toggle`, `secret`, `parameter` -- UX & Interface: `ui`, `ux`, `screen`, `form`, `flow`, `command`, `cli`, `option` -- Testing & Quality: `test`, `e2e`, `regression`, `coverage`, `validation`, `acceptance` - -## 정규화 규칙 -- 사전에 없는 표현이 입력에 등장하면 의미가 유사한 사전 키워드로 1회 정규화해 근거에 표기한다. -- 예: `권한` -> `permission`, `과금` -> `billing` - -## 동점 처리 -- 동일 점수 후보가 여러 개면 최신 Change Log 항목이 있는 REQ를 우선한다. -- 여전히 동점이면 현재 active 상태(`Approved`/`Implemented`) REQ를 우선한다. -- 추가 동점이면 REQ ID가 작은 항목을 우선한다. - -## 출력 권장 -- 후보는 최대 5개 제시 -- 각 후보에 `REQ ID`, `신뢰도`, `점수`, `근거 키워드`, `영향 영역`을 포함 -- 확정 전에는 `Immediate Questions`로 최종 대상 REQ 선택을 먼저 요청 diff --git a/.github/instructions/requirement-scope-decision.instructions.md b/.github/instructions/requirement-scope-decision.instructions.md deleted file mode 100644 index b3df18a..0000000 --- a/.github/instructions/requirement-scope-decision.instructions.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -description: "Use when evaluating whether a new feature or change request requires a new or updated SRS requirement, a technical spec update only, or needs owner decision. Apply before starting requirement drafting." -applyTo: "docs/SRS/**/*.md" ---- -# Requirement Scope Decision Guidelines - -## 목적 -새 기능 요청이나 변경 사항이 들어왔을 때, SRS에 등록해야 하는지(신규 또는 기존 REQ 수정) 아니면 technical spec / implementation note만 업데이트하면 되는지를 먼저 판정한다. -이 판정을 거침으로써 불필요한 REQ 생성을 방지하고, SRS에 등록돼야 할 변경이 누락되는 것을 예방한다. - -## 판정 결과 분류 (4종) -1. **New SRS requirement needed** — 새 REQ 문서를 작성해야 한다. -2. **Update existing SRS requirement** — 기존 REQ 문서를 수정해야 한다. -3. **Technical spec only** — SRS 변경 없이 technical spec / implementation note만 업데이트한다. -4. **Needs owner decision** — 운영 리스크나 승인 기준이 불명확하여 소유자(Owner) 결정이 필요하다. - -## SRS 대상 판정 기준 (다음 중 하나라도 해당하면 SRS) - -### 외부 계약 변화 -- 사용자 또는 외부 시스템이 관찰하는 계약이 바뀌는 경우 -- API / CLI / UI / Event / Data schema / Config / Deployment / Integration 요구사항이 변경되는 경우 - -### 측정 가능한 Non-Functional 변경 -- 성능, 보안, 가용성, 확장성 등 측정 기준이 추가되거나 변경되는 경우 - -### Acceptance Criteria 및 승인 게이트 필요 -- 변경 사항에 대해 명시적 수락 기준과 승인 이력이 필요한 경우 - -## Technical Spec Only 판정 기준 (다음에 해당하면 SRS 불필요) -- 내부 리팩터링 (외부 계약 변화 없음) -- 라이브러리 교체 (동일한 외부 인터페이스 유지) -- 코드 구조 / 패턴 변경 (기능 동작 동일) -- 외부 계약을 바꾸지 않는 구현 최적화 - -## Borderline / Needs Owner Decision 판정 기준 -- 운영 리스크나 승인 책임이 불명확한 경우 -- 제품 의도가 불분명하거나 범위가 애매한 경우 -- 위 기준만으로 분류가 확정되지 않는 경우 - -## New vs. Update 구분 기준 -- 동일 기능/계약 범위의 변경 → `Update existing SRS requirement` -- 새 기능/계약/인터페이스의 추가 → `New SRS requirement needed` -- 기존 REQ 검색 시 `docs/SRS/index.md`의 Requirement Register를 먼저 확인한다. - -## 판정 출력 형식 -판정 결과는 항상 다음 항목을 포함해야 한다. -1. **Decision** — 위 4종 중 하나 -2. **Why** — 판정 근거 (구체적으로 어떤 기준에 해당하는지) -3. **Recommended Type** — SRS 대상인 경우, 해당 requirement type (Interface / Data / Deployment 등) -4. **Existing REQ candidates** — Update 가능성이 있는 기존 REQ ID 목록 (해당 시) -5. **Next Action** — 판정 결과에 따른 다음 단계 안내 -6. **Open Questions** — 추가 확인이 필요한 사항 (없으면 `없음`) diff --git a/.github/instructions/srs.instructions.md b/.github/instructions/srs.instructions.md deleted file mode 100644 index d2e3533..0000000 --- a/.github/instructions/srs.instructions.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -description: "Use when writing or editing SRS documents, requirement files, acceptance criteria, change logs, or impacted area entries under docs/SRS. Ensures requirement quality, traceability, and testable wording." -applyTo: "docs/SRS/**/*.md" ---- -# SRS Document Guidelines - -## Requirement 타입 분류 - -### 분류 의사결정 규칙 (우선순위 순) -1. **Installation**: "시스템 설치, 환경 구성, 초기화 절차를 정의하는가?" → YES면 Installation - - 예: Docker 이미지 빌드, 데이터베이스 마이그레이션, 사전 조건 설정 -2. **Interface**: "사용자가 직접 조작하거나 실행하는 방식을 정의하는가?" → YES면 Interface - - 예: CLI 명령어, API 엔드포인트, 웹 UI 기능, 제어 방식 -3. **Configuration**: "시스템 또는 기능의 설정 옵션, 커스터마이징을 정의하는가?" → YES면 Configuration - - 예: 설정 파라미터, 옵션 항목, 환경 변수, 커스터마이징 포인트 -4. **Data**: "데이터 저장, 로드, 구조, 파일 형식, 스키마를 정의하는가?" → YES면 Data - - 예: 설정 파일 형식, 데이터베이스 스키마, API 요청/응답 구조 -5. **Exception & Error Handling**: "오류 처리, 예외 복구, 재시도 전략을 중심으로 하는가?" → YES면 Exception & Error Handling - - 예: 네트워크 오류 재시도, 데이터 검증 실패 처리, 복구 절차 -6. **Deployment**: "배포 방식, 운영 절차, 배포 환경을 정의하는가?" → YES면 Deployment - - 예: 컨테이너 배포, 스택 관리, 운영 가이드 -7. **Documentation**: "문서 작성, 가이드, 매뉴얼, 안내서를 정의하는가?" → YES면 Documentation - - 예: 사용자 가이드, API 문서, 운영 매뉴얼, 튜토리얼 -8. **Integration**: "외부 시스템 연동, 플러그인, 확장 기능을 정의하는가?" → YES면 Integration - - 예: 3rd party 서비스 연동, 플러그인 인터페이스, 확장 메커니즘 -9. **Migration**: "데이터 이전, 버전 업그레이드, 마이그레이션을 정의하는가?" → YES면 Migration - - 예: 데이터 마이그레이션, 버전 호환성, 업그레이드 절차 -10. **Testing**: "테스트 전략, QA 요구사항, 검증 방식을 정의하는가?" → YES면 Testing - - 예: 테스트 커버리지, 회귀 테스트, 수락 테스트 기준 -11. **Non-Functional**: 위 모든 경우에 해당하지 않으면 → Non-Functional - - 예: 성능 목표, 보안 정책, 확장성, 호환성, 신뢰성 요구사항 - -**주의**: requirement가 여러 분류에 해당하면 주된 제약 또는 목적에 따라 하나로만 선택한다. 예를 들어: -- "Docker Compose로 배포할 수 있어야 한다" → Deployment -- "배포 시 환경 변수로 설정 가능해야 한다" → Configuration -- "배포 전에 다음 점검 항목을 검증해야 한다" → Testing/Documentation - -- `docs/SRS/index.md`는 SRS 운영 기준과 Requirement Register를 관리하는 문서로 유지한다. -- `docs/SRS/REQ-XXX_*.md`는 하나의 requirement만 다루고, 다른 requirement 내용을 섞지 않는다. 파일은 Type별 폴더에 저장한다. 예: `docs/SRS/Deployment/REQ-001_docker-compose-release.md` -- 새 requirement 번호의 source of truth는 `docs/SRS/index.md`의 `Next Requirement ID`다. -- requirement 본문에는 최소한 `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log`가 포함되어야 한다. -- `Acceptance Criteria`는 관찰 가능하고 test 가능한 문장으로 작성한다. -- 모호한 표현("개선한다", "적절히 처리한다", "최적화한다")은 측정 기준 없이 사용하지 않는다. -- requirement를 수정하면 해당 REQ 파일의 `Change Log`도 함께 갱신한다. -- `Impacted Area`는 `Docs`, `Modules`, `Tests`를 모두 검토한 뒤 작성한다. 해당 항목이 없으면 `없음`으로 명시한다. -- implementation issue/PR를 생성하거나 병합할 때는 `docs/SRS/traceability-matrix.md`에서 해당 REQ 행의 `Issue/PR`, `Tests`, `Status`를 갱신한다. -- 새로운 Type으로 requirement를 추가할 때는 `docs/SRS//` 폴더 하위에 저장한다. 아직 없는 Type 폴더면 생성한다. -- 새로운 requirement를 추가하면 `docs/SRS/index.md`의 Requirement Register에도 같은 ID를 추가한다. -- 새로운 requirement를 추가하거나 Register가 바뀌면 `docs/SRS/index.md`의 `Next Requirement ID`도 함께 갱신한다. -- `docs/SRS/index.md`의 `Recent Change Log Summary`는 최신 5건만 유지하고 최신 항목을 위에 둔다. -- requirement ID와 파일명 prefix는 일치해야 한다. 예: `REQ-002`는 `docs/SRS//REQ-002_.md`. -- requirement 상태를 `Approved` 또는 `Implemented`로 바꿀 때는 acceptance criteria와 change history가 누락되지 않았는지 먼저 검토한다. -- requirement 상태를 전환할 때는 `docs/SRS/index.md`의 `Status Transition Evidence` 기준을 따른다. diff --git a/.github/prompts/approve-requirement.prompt.md b/.github/prompts/approve-requirement.prompt.md deleted file mode 100644 index 3bf3267..0000000 --- a/.github/prompts/approve-requirement.prompt.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -description: "Use when: decide whether a Proposed requirement is ready to move to Approved and generate the required status-change edits" -name: "Approve Requirement" -argument-hint: "Paste a Proposed REQ file or select requirement text" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서가 `Proposed`에서 `Approved`로 전환 가능한지 검토한다. - -입력: -${input} - -검토 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 의 `Status Transition Evidence`와 해당 requirement 문서 구조다. -- `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log` 존재 여부를 검토한다. -- Type이 SRS index의 분류 도표에 정의된 타입 중 정확하게 하나로 지정되었는지 확인한다. -- `Acceptance Criteria`가 구현 기준으로 충분히 구체적인지 판단한다. -- 중요한 assumption 또는 open question이 남아 있으면 승인 blocker로 본다. -- 승인 가능하면 status 변경과 Change Log 추가 초안을 함께 제안한다. -- 승인 불가하면 blocker를 우선순위 순으로 정리한다. - -출력 형식: -1. Approval Decision: `Approved-ready` 또는 `Not ready` -2. Blockers -3. Suggested Edits -4. Approval Change Log Draft diff --git a/.github/prompts/complete-requirement.prompt.md b/.github/prompts/complete-requirement.prompt.md deleted file mode 100644 index ea0de9a..0000000 --- a/.github/prompts/complete-requirement.prompt.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -description: "Use when: complete a partial SRS requirement draft into a full REQ file with acceptance criteria and impacted area" -name: "Complete Requirement Draft" -argument-hint: "Paste a partial requirement draft or describe the feature" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기반으로 SRS requirement 초안을 완성한다. - -입력: -${input} - -작업 규칙: -- SRS 기준 문서는 [SRS index](../../docs/SRS/index.md) 와 관련 [REQ template](../../docs/SRS/REQ-template.md) 를 따른다. -- 입력에 `REQ-XXX` 형식의 명시적 ID가 없으면, [SRS index](../../docs/SRS/index.md)의 `Next Requirement ID`를 우선 읽어 그 값을 사용한다. `Next Requirement ID`가 없거나 신뢰할 수 없으면 `Requirement Register`와 기존 REQ 파일들 기준의 최고 번호에 1을 더한 값을 사용한다. -- 사용자가 이미 특정 REQ ID를 명시했으면 그 ID를 유지하고 임의로 바꾸지 않는다. -- requirement의 유형을 인식하고 `Type` 필드를 정확하게 지정한다. 분류는 [SRS index](../../docs/SRS/index.md)의 "Requirement 작성 규칙" 또는 [SRS instructions](../../.github/instructions/srs.instructions.md)의 "분류 의사결정 규칙"을 따른다. -- 최종 `Type`과 `REQ ID`를 이용해 추천 파일 경로를 함께 제시한다. 형식은 `docs/SRS//REQ-XXX_.md`를 사용한다. -- ``는 requirement 제목을 기반으로 소문자 kebab-case 영문으로 생성한다. 공백은 `-`로 치환하고, 영문/숫자/`-`만 남긴다. -- 제목이 한글이거나 slug 생성이 불명확하면 의미를 유지하는 짧은 영문 slug를 보수적으로 제안한다. -- 사용자가 제공한 의도와 핵심 동작을 보존한다. -- 빠진 항목은 보수적으로 보완하되, 추정이 필요한 내용은 `Notes`에 assumption으로 분리한다. -- `Acceptance Criteria`는 반드시 test 가능하게 작성한다. -- `Impacted Area`는 Docs, Modules, Tests로 구분한다. -- 새로 할당한 ID를 문서 제목 `# REQ-XXX: ...`에 반영한다. -- 결과는 REQ 파일에 바로 붙여넣을 수 있는 Markdown 형식으로만 출력한다. -- 코드 구현 계획은 포함하지 않는다. - -출력 형식: -1. `Recommended File Path: docs/SRS//REQ-XXX_.md` 한 줄 -2. 실제 할당된 ID가 반영된 `# REQ-XXX: ...` 형식의 전체 문서 본문 -3. `## Change Log` 아래에 초안 생성용 `CHG-YYYYMMDD-01` 항목 1개 포함 diff --git a/.github/prompts/create-implementation-issue.prompt.md b/.github/prompts/create-implementation-issue.prompt.md deleted file mode 100644 index 1566340..0000000 --- a/.github/prompts/create-implementation-issue.prompt.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -description: "Use when: create an implementation plan and GitHub issue draft from an Approved requirement" -name: "Create Implementation Issue" -argument-hint: "Paste an Approved REQ file" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 `Approved` requirement 문서를 기반으로 implementation issue 초안을 생성한다. - -입력: -${input} - -작업 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 와 해당 REQ 문서다. -- 출력 언어는 기본적으로 한국어로 작성한다. -- section 제목, 설명, 작업 지시문은 한국어를 우선 사용하되, 중요한 technical term는 영어를 유지하거나 한국어 뒤 괄호로 병기한다. 예: `AI Agent Work`, `Human Verification Checklist`, `Acceptance Criteria`, `Open Questions`, `Risk`, `Definition of Done`. -- 코드 식별자, 파일 경로, 명령어, API 이름, 테스트 이름, UI 라벨 등 구현 대상과 직접 연결되는 literal은 영어 원문을 유지한다. -- requirement의 `Acceptance Criteria`를 구현 task로 분해한다. -- task는 구현 단위로 나누고, 각 task를 `AI Agent가 완료 가능한 작업`과 `Human이 수행해야 하는 작업`으로 구분한다. -- test task를 반드시 포함한다. -- 각 acceptance criteria 또는 task에 대해 다음을 판단한다: - - AI Agent가 코드 변경과 자동 검증까지 완료할 수 있는가 - - Human이 직접 확인해야 하는 수동 검증이 남는가 -- Human이 직접 확인해야 하는 항목이 있으면 `Human Verification Checklist`에 구체적인 확인 절차와 기대 결과를 작성한다. -- AI Agent가 자체적으로 테스트를 완료할 수 있어 Human 확인이 불필요하면 `Human Verification Checklist`에 `No manual verification required.`를 명시하고 그 근거를 `AI Validation Summary`에 적는다. -- `Impacted Area`를 기반으로 module/docs/test 변경 범위를 제시한다. -- 미해결 가정이나 리스크가 있으면 issue 본문에 `Open Questions`와 `Risks`로 명시한다. -- 출력은 GitHub issue에 바로 붙여 넣을 수 있는 Markdown 형식으로만 작성한다. - -출력 형식: -1. Issue Title -2. 배경 (Context) -3. 범위 (Scope) -4. AI Agent 작업 (AI Agent Work) 체크리스트 -5. 사람 작업 (Human Work) 체크리스트 -6. Acceptance Criteria Mapping (REQ 기준) -7. AI 검증 요약 (AI Validation Summary) -8. 사람 확인 체크리스트 (Human Verification Checklist) -9. 열린 질문 (Open Questions) -10. 리스크 (Risks) -11. 완료 조건 (Definition of Done) -12. GitHub Issue Create Command (`gh issue create`) 예시 diff --git a/.github/prompts/decide-requirement-scope.prompt.md b/.github/prompts/decide-requirement-scope.prompt.md deleted file mode 100644 index 4a847ec..0000000 --- a/.github/prompts/decide-requirement-scope.prompt.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -description: "Use when: evaluate whether a new feature or change request needs a new SRS requirement, an update to an existing one, a technical spec update only, or owner decision — before starting requirement drafting" -name: "Decide Requirement Scope" -argument-hint: "Describe the feature or change request: include feature description, issue body, and/or PR/diff summary" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기반으로, 이 변경이 SRS 대상인지 아닌지를 판정한다. - -입력: -${input} - -판정 규칙: -- 기준 문서는 [requirement-scope-decision instructions](../../.github/instructions/requirement-scope-decision.instructions.md) 와 [SRS index](../../docs/SRS/index.md) 의 Requirement Register다. -- 다음 4가지 결과 중 하나를 반드시 반환한다. - 1. `New SRS requirement needed` - 2. `Update existing SRS requirement` - 3. `Technical spec only` - 4. `Needs owner decision` -- 판정 기준을 순서대로 적용한다. - - 사용자/외부 시스템이 관찰하는 계약(API, CLI, UI, Event, Data schema, Config, Deployment, Integration)이 바뀌는가? → YES면 SRS - - 측정 가능한 Non-Functional requirement(성능, 보안, 가용성 등)가 추가/변경되는가? → YES면 SRS - - Acceptance Criteria 및 명시적 승인 게이트가 필요한가? → YES면 SRS - - 위 기준에 모두 해당하지 않으면서 내부 리팩터링·라이브러리 교체·코드 구조 변경·구현 최적화인가? → Technical spec only - - 위 기준만으로 분류가 확정되지 않거나 운영 책임/리스크가 불명확한가? → Needs owner decision -- SRS 대상으로 판정되면, 신규 기능/계약인지 기존 REQ 범위 내 변경인지를 구분한다. - - 기존 REQ에 해당하는 경우 [SRS index](../../docs/SRS/index.md) 의 Requirement Register에서 후보 REQ ID를 찾아 제시한다. -- Technical spec only로 판정되면, 불필요한 REQ 생성을 명시적으로 피하도록 안내한다. -- 결과에는 항상 `Why`와 `Next Action`을 포함한다. -- `Open Questions`가 없으면 `없음`으로 명시한다. - -출력 형식: -1. **Decision**: `New SRS requirement needed` / `Update existing SRS requirement` / `Technical spec only` / `Needs owner decision` 중 하나 -2. **Why**: 판정 근거 — 어떤 기준에 해당하는지 구체적으로 설명 -3. **Recommended Type**: (SRS 대상인 경우만) Interface / Data / Deployment / Configuration 등 해당 requirement type -4. **Existing REQ candidates**: (Update 가능성이 있는 경우만) 관련 REQ ID와 제목 목록. 해당 없으면 생략 -5. **Next Action**: 판정 결과에 따른 다음 단계 안내 -6. **Open Questions**: 추가 확인이 필요한 사항. 없으면 `없음` diff --git a/.github/prompts/mark-requirement-implemented.prompt.md b/.github/prompts/mark-requirement-implemented.prompt.md deleted file mode 100644 index b1567f5..0000000 --- a/.github/prompts/mark-requirement-implemented.prompt.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -description: "Use when: decide whether an Approved requirement is ready to move to Implemented and generate the required status-change edits" -name: "Mark Requirement Implemented" -argument-hint: "Paste an Approved REQ file and implementation evidence" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서와 implementation evidence를 검토해 `Approved`에서 `Implemented`로 전환 가능한지 판단한다. - -입력: -${input} - -검토 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 의 `Status Transition Evidence`와 해당 requirement 문서 구조다. -- requirement의 `Acceptance Criteria`가 implementation evidence, tests, 또는 manual verification으로 충족되었는지 확인한다. -- 충족되지 않은 acceptance criteria가 있으면 구현 완료로 판정하지 않는다. -- 구현 완료 가능하면 status 변경, 잔여 리스크, Change Log 추가 초안을 함께 제안한다. -- 구현 완료 불가하면 미충족 기준과 필요한 검증 항목을 정리한다. - -출력 형식: -1. Implementation Decision: `Implemented-ready` 또는 `Not ready` -2. Unverified Criteria -3. Suggested Edits -4. Implementation Change Log Draft -5. Residual Risk diff --git a/.github/prompts/reopen-requirement-status.prompt.md b/.github/prompts/reopen-requirement-status.prompt.md deleted file mode 100644 index 695c628..0000000 --- a/.github/prompts/reopen-requirement-status.prompt.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -description: "Use when: move Approved, Implemented, or Deprecated requirements back to Proposed (or Re-review) with decision checkpoints, conflict candidates, and fallback REQ discovery" -name: "Reopen Requirement Status" -argument-hint: "(선택) REQ-XXX, REQ-XXX; 변경 의도와 범위 설명" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기준으로 지정된 requirement를 재검토 상태로 되돌리는 변경안을 만든다. - -입력: -${input} - -입력 해석 규칙: -- 입력 형식은 기본적으로 `REQ-XXX, REQ-XXX; 이렇게 이렇게 변경할거야.` 를 따른다. -- `;` 앞은 대상 REQ ID 목록, 뒤는 변경 의도/배경으로 해석한다. -- 구분자는 쉼표/공백을 모두 허용하되, 최종적으로 중복 없는 REQ ID 목록으로 정규화한다. -- 입력에 REQ ID가 없으면 전체 입력을 변경 의도/배경으로 간주한다. - -작업 규칙: -- 기준 문서는 `docs/SRS/index.md`, 해당 REQ 파일, [SRS instructions](../../.github/instructions/srs.instructions.md), [Reopen requirement discovery instructions](../../.github/instructions/reopen-requirement-discovery.instructions.md) 다. -- 대상 REQ ID가 명시되지 않으면 `docs/SRS/index.md` Requirement Register와 REQ 파일 제목/Intent/Requirement/Impacted Area를 기반으로 관련 후보를 탐색한다. -- 후보 탐색, 점수화, 신뢰도, 동점 처리, 도메인 키워드 정규화는 [Reopen requirement discovery instructions](../../.github/instructions/reopen-requirement-discovery.instructions.md)를 그대로 따른다. -- REQ 미지정 시 후보는 최대 5개까지 제시하고, 신뢰도(High/Medium/Low)와 근거(키워드/영향 영역)를 함께 제시한다. -- REQ 미지정 시 확정 전에는 상태 변경안을 단정하지 말고, `Immediate Questions`에서 최종 대상 REQ 확인 질문을 먼저 제시한다. -- 사용자가 후보 중 선택하면 선택된 REQ 기준으로 상태 되돌림 변경안을 완성한다. -- 대상 REQ의 현재 상태가 `Approved`, `Implemented`, `Deprecated` 중 하나인지 확인한다. -- 상태 되돌림의 목표 상태는 기본 `Proposed`로 제안한다. -- 팀이 `재검토 상태`를 별도로 운용하면 목표 상태를 `Re-review`(또는 팀 표기)로 제안할 수 있다. 단, 최종 상태값은 사용자 결정 항목으로 분리한다. -- 각 REQ마다 왜 되돌려야 하는지(계약 변경, acceptance criteria 변경, 리스크 발견, 구현 불일치 등) 근거를 1-2줄로 요약한다. -- 변경 전/후 영향으로 충돌 가능성이 있는 REQ 후보를 `docs/SRS/index.md` Requirement Register 기준으로 제시한다. -- 충돌 후보는 다음 기준으로 찾는다: 동일 기능 영역, 동일 인터페이스/API/CLI/데이터 스키마, 같은 모듈/문서 영향 범위, 기존 acceptance criteria 중복/상충. -- 사용자가 결정해야 할 항목을 별도 섹션으로 분리한다. -- 결정 누락 시 즉시 물어야 할 질문을 먼저 제시한다. -- 실제 문서 변경을 수행하는 대신, 바로 적용 가능한 수정안(상태, Change Log, impacted area 재검토 포인트)을 제시한다. - -출력 형식: -1. Reopen Decision -2. Immediate Questions (없으면 `없음`) -3. Detected REQ Candidates (REQ 미지정인 경우 필수, 지정된 경우 생략 가능) -4. User Decisions Required -5. Suggested Status Changes (REQ별: `from -> to`) -6. Conflict Candidate REQs (REQ별 후보 ID와 충돌 이유) -7. Suggested REQ Edits (Status, Intent/Requirement/Acceptance Criteria 영향, Change Log 초안) -8. Next Action diff --git a/.github/prompts/sync-srs-index.prompt.md b/.github/prompts/sync-srs-index.prompt.md deleted file mode 100644 index e8ecc03..0000000 --- a/.github/prompts/sync-srs-index.prompt.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -description: "Use when: update docs/SRS/index.md Requirement Register or Recent Change Log Summary after adding or editing REQ files" -name: "Sync SRS Index" -argument-hint: "Paste changed REQ files or describe what changed in SRS" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기준으로 `docs/SRS/index.md`를 갱신하기 위한 제안을 만든다. - -입력: -${input} - -작업 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 이다. -- 새 requirement가 추가되었거나 Register가 바뀌면 `Requirement Register`의 최고 번호를 기준으로 다음 번호를 계산해 `Next Requirement ID`도 함께 갱신한다. -- 새 requirement가 추가되었으면 `Requirement Register`에 들어갈 행을 만든다. -- 기존 requirement가 수정되었으면 `Recent Change Log Summary`에 들어갈 요약 항목을 만든다. -- 기존 구조를 바꾸지 말고, 필요한 행과 요약만 제안한다. -- `Next Requirement ID`는 `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 큰 값이어야 한다. -- `Recent Change Log Summary`는 최신 변경을 위에 두고, 짧은 요약 1줄로 작성한다. -- `Recent Change Log Summary`는 최신 5건만 유지하도록 제안한다. 5건을 초과하면 제거 또는 대체해야 할 가장 오래된 항목도 함께 설명한다. -- 출력은 사람이 `docs/SRS/index.md`에 바로 반영할 수 있는 Markdown 조각으로만 작성한다. - -출력 형식: -1. `Next Requirement ID`에 반영할 Markdown bullet -2. `Requirement Register`에 추가 또는 수정할 Markdown table row -3. `Recent Change Log Summary`에 추가할 Markdown section -4. 필요한 경우 only, index에서 제거 또는 갱신해야 할 기존 항목 설명 diff --git a/.github/prompts/sync-technical-specs.prompt.md b/.github/prompts/sync-technical-specs.prompt.md deleted file mode 100644 index 3cdb1c6..0000000 --- a/.github/prompts/sync-technical-specs.prompt.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -description: "Use when: verify one or more REQ IDs are Approved and create or update the matching docs/technical specifications" -name: "Sync Technical Specs" -argument-hint: "Enter REQ IDs, e.g. REQ-011, REQ-007, REQ-012" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력으로 지정된 requirement ID 목록에 대해, 승인 상태를 확인하고 해당하는 `docs/technical/` 문서를 실제로 갱신한다. - -입력: -${input} - -작업 규칙: -- 입력에서 `REQ-XXX` 목록을 추출한다. 쉼표 또는 공백으로 구분된 여러 ID를 지원한다. -- 먼저 `docs/SRS/index.md`의 `Requirement Register`와 각 REQ 파일의 `Status`를 함께 확인한다. -- 두 곳 모두 `Approved`인 requirement만 기술명세 업데이트 대상으로 포함한다. -- 하나라도 `Approved`가 아니거나 파일을 찾지 못하면 해당 REQ는 blocker로 보고하고, 그 REQ에 대한 technical doc은 수정하지 않는다. -- `docs/technical/index.md`와 각 technical doc 상단의 `Related REQ`를 기준으로 기존 연결 문서를 찾는다. -- 연결 문서가 있으면 현재 승인된 requirement에 맞게 내용을 실제로 업데이트한다. -- 연결 문서가 없으면 `docs/technical/` 아래에 새 문서를 생성하고 `docs/technical/index.md`에 항목을 추가한다. -- 기술 문서는 SRS를 source of truth로 따르며, 외부 계약이나 acceptance criteria를 재정의하지 않는다. 대신 구현 상세(모듈 책임, 파일 위치, 설정/환경값, 런타임 흐름, validation notes, open implementation points)를 정리한다. -- 문서 상단에는 반드시 `- Related REQ: ...`를 유지하거나 추가한다. -- `REQ-011`, `REQ-007`, `REQ-012`는 다음 문서를 우선 후보로 사용한다: - - `REQ-011` → `docs/technical/project-structure-baseline.md` - - `REQ-007` → `docs/technical/python-runtime-and-packaging.md` - - `REQ-012` → `docs/technical/settings-and-env-loading.md` -- `docs/technical/index.md`의 문서 목록과 연관 REQ 표도 문서 생성/연결 변경 사항에 맞게 함께 갱신한다. -- 실제 파일 변경을 수행한 뒤, 변경된 파일과 각 REQ의 승인 확인 결과를 요약한다. - -출력 형식: -1. Approval Check -2. Technical Spec Updates -3. Files Changed -4. Remaining Blockers or Notes diff --git a/.github/prompts/validate-requirement.prompt.md b/.github/prompts/validate-requirement.prompt.md deleted file mode 100644 index bbb9ba1..0000000 --- a/.github/prompts/validate-requirement.prompt.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -description: "Use when: review an SRS requirement for ambiguity, missing acceptance criteria, missing impacted area, and change-log gaps" -name: "Validate Requirement Quality" -argument-hint: "Paste a REQ file or select requirement text" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서를 검토한다. - -입력: -${input} - -검토 규칙: -- 기준은 [SRS index](../../docs/SRS/index.md) 와 관련 requirement 문서 구조를 따른다. -- 다음 항목을 우선 검토한다: 모호한 표현, test 불가능한 문장, 누락된 acceptance criteria, 누락된 impacted area, 상태/유형/우선순위/owner 누락, Type이 의사결정 규칙에 따라 정확하게 지정되었는지, Change Log 누락 또는 불일치. - - Type 분류 규칙: 사용자 조작 방식 → Interface, 데이터 저장/구조 → Data, 오류 처리 → Exception & Error Handling, 그 외 → Non-Functional -- 중요한 문제부터 우선순위 순으로 정리한다. -- 문제가 없으면 `No critical findings.` 로 시작한다. -- requirement 본문을 직접 다시 작성하지 말고, 수정 제안 위주로 답한다. - -출력 형식: -1. Findings -2. Open Questions -3. Suggested Edits diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 4af7096..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,24 +0,0 @@ -## Requirement Traceability - -- Requirement ID: REQ-XXX -- Requirement Link: docs/SRS//REQ-XXX_.md -- Related Issue: # - -## Acceptance Criteria Coverage -- [ ] AC1: -- [ ] AC2: - -## Test Evidence -- Automated: - - -- Manual: - - - - If not required: `No manual verification required.` - -## Impacted Area Check -- Docs: -- Modules: -- Tests: - -## Release Notes Input -- [ ] Added REQ ID to release notes (or tracked release note draft) diff --git a/.vendor/myagent-kit/README.md b/.vendor/myagent-kit/README.md deleted file mode 100644 index 394215f..0000000 --- a/.vendor/myagent-kit/README.md +++ /dev/null @@ -1,152 +0,0 @@ -# myagent-kit - -Reusable Copilot Development workflow assets. - -## Contents -- `base/.github/prompts`: reusable prompts -- `base/.github/instructions`: reusable file instructions -- `base/.github/ISSUE_TEMPLATE`: reusable issue templates -- `base/.github/copilot-instructions.md`: managed Copilot baseline gate template (opt-in merge) -- `base/.github/pull_request_template.md`: PR traceability gate template -- `templates/docs/SRS/REQ-template.md`: REQ starter template -- `templates/docs/SRS/TRACEABILITY-template.md`: traceability matrix starter template -- `bootstrap/install.sh`: copy base assets into a target project - -### Pre-flight Scope Decision (Step 0) -새 기능이나 변경 요청이 들어오면, requirement drafting을 시작하기 **전에** 먼저 아래 명령으로 SRS 대상 여부를 판정한다. - -| Asset | 용도 | -|---|---| -| `base/.github/prompts/decide-requirement-scope.prompt.md` | 변경 요청을 입력받아 4종 판정 결과 반환 | -| `base/.github/instructions/requirement-scope-decision.instructions.md` | 판정 기준 및 출력 형식 가이드 | -| `base/.github/prompts/reopen-requirement-status.prompt.md` | `Approved/Implemented/Deprecated` requirement를 `Proposed`(또는 `Re-review`)로 되돌리기 위한 결정/충돌 검토 | -| `base/.github/instructions/reopen-requirement-discovery.instructions.md` | REQ 미지정 입력 시 후보 탐색(키워드/점수/동점 처리) 커스터마이징 가이드 | - -**판정 결과 4종** -1. `New SRS requirement needed` — 새 REQ 문서 작성 -2. `Update existing SRS requirement` — 기존 REQ 문서 수정 -3. `Technical spec only` — SRS 변경 없이 technical spec만 업데이트 -4. `Needs owner decision` — 운영 리스크/책임이 불명확하여 소유자 결정 필요 - -## Install into a project - -```bash -bash .vendor/myagent-kit/bootstrap/install.sh /path/to/project -``` - -변경 적용 없이 계획만 확인하려면 dry-run을 사용한다. - -```bash -bash .vendor/myagent-kit/bootstrap/install.sh /path/to/project --dry-run -``` - -옵션: 기존 `.github/copilot-instructions.md`를 덮어쓰지 않고 병합하려면 opt-in 플래그를 사용한다. - -```bash -bash .vendor/myagent-kit/bootstrap/install.sh /path/to/project --merge-copilot-instructions -``` - -병합 정책 요약: -- marker 이름은 `myagent-kit`로 고정된다. -- 기존 파일이 있으면 `.github/copilot-instructions.md.bak`으로 최근 1개 백업 후 병합한다. -- 병합은 `myagent-kit` 관리 섹션만 갱신하고 `project-specific` 섹션은 유지한다. -- marker 쌍이 손상된 경우 병합을 중단하고 백업본으로 롤백한다. - -In subtree-based usage: - -```bash -git subtree add --prefix=.vendor/myagent-kit https://github.com//myagent-kit.git main --squash -bash .vendor/myagent-kit/bootstrap/install.sh . -bash .vendor/myagent-kit/bootstrap/install.sh . --dry-run -bash .vendor/myagent-kit/bootstrap/install.sh . --merge-copilot-instructions -bash .vendor/myagent-kit/bootstrap/install.sh . --merge-copilot-instructions --dry-run -``` - -## 운영 규칙 - -- SRS는 계약 문서, 구현 문서는 실행 기록으로 분리합니다. -- 모든 요구사항에 고유 ID를 부여합니다. 예: REQ-001 -- 구현/테스트/릴리즈까지 같은 ID를 계속 사용합니다. -- 상태 기반으로 관리합니다: Draft → Approved → Implemented → Verified → Released - -## 추적/운영 표준 - -### 1) 식별자 규칙 -- Requirement ID는 `REQ-XXX` 형식을 사용합니다. 예: `REQ-001` -- Acceptance Criteria는 requirement 내부에서 `AC1`, `AC2`처럼 번호를 고정합니다. -- 이슈/PR 제목에는 requirement ID를 포함합니다. 예: `[REQ-001] ...` - -### 2) 증거(Evidence) 연결 규칙 -- 구현 이슈에는 `Acceptance Criteria Mapping`을 작성합니다. (`ACx -> Task`) -- PR에는 어떤 AC가 어떤 변경으로 충족되었는지 명시합니다. -- 테스트 이름 또는 테스트 설명에도 requirement ID를 포함합니다. -- 릴리즈 노트에는 포함된 requirement ID 목록을 남깁니다. - -### 3) 추적 문서 운영 -- 추적 매트릭스는 `docs/SRS/traceability-matrix.md`로 운영합니다. -- 초기 작성은 `templates/docs/SRS/TRACEABILITY-template.md`를 복사해 사용합니다. -- 매트릭스의 각 행은 최소한 `REQ ID`, `AC`, `Issue/PR`, `Test`, `Status`를 채웁니다. - -### 4) 최소 운영 절차 -1. Requirement 작성 및 ID 확정 -2. Requirement를 `Approved`로 전환 -3. Implementation Issue 생성 (AC 매핑 포함) -4. PR 생성 (REQ/AC 충족 근거 기입) -5. 테스트 증거 업데이트 -6. Requirement 상태를 `Implemented`/`Verified`로 전환 -7. 릴리즈 시 매트릭스와 릴리즈 노트에 REQ ID 반영 - -## myagent-kit Development Life Cycle - -### Phase 0. Workspace Bootstrap -- 프로젝트에 myagent-kit 자산을 설치한다. -- `bootstrap/install.sh`를 실행해 `.github` 워크플로 자산을 동기화한다. - -### Phase 0.5. Pre-flight Scope Decision *(새 기능/변경 요청 진입 시 필수)* -- `decide-requirement-scope.prompt.md`로 변경 요청이 SRS 대상인지 먼저 판정한다. -- 판정 결과에 따라 다음 단계를 결정한다. - - `New SRS requirement needed` → Phase 1로 진행 - - `Update existing SRS requirement` → 해당 REQ 파일을 수정하고 Phase 2로 진행 - - `Technical spec only` → SRS 변경 없이 technical spec / implementation note만 업데이트 - - `Needs owner decision` → 소유자와 협의 후 위 3가지 중 하나로 재판정 - -### Phase 1. Requirement Drafting -- `templates/docs/SRS/REQ-template.md`로 REQ 문서를 작성한다. -- REQ ID(`REQ-XXX`)를 확정하고, `Acceptance Criteria`를 test 가능한 문장으로 작성한다. - -### Phase 2. Requirement Quality Gate -- `validate-requirement.prompt.md`로 requirement 품질 검토를 수행한다. -- 모호성, 누락 필드, impacted area, change log 누락을 수정한다. - -### Phase 3. Requirement Approval Gate -- `approve-requirement.prompt.md`로 `Proposed -> Approved` 전환 가능 여부를 확인한다. -- 승인 이력(Change Log)을 남기고 상태를 `Approved`로 변경한다. - -### Phase 3.5. Requirement Reopen / Re-review -- 구현 전/후에 계약 변경, acceptance criteria 재정의, 리스크 재평가가 필요하면 `reopen-requirement-status.prompt.md`를 사용한다. -- 입력 예시: `REQ-003, REQ-011; API 응답 필드 계약을 다시 정리해야 해서 재검토 필요`. -- 결과에서 사용자 결정 항목(목표 상태: `Proposed` vs `Re-review`)과 충돌 후보 REQ를 먼저 확인한 뒤 REQ 문서/인덱스를 수정한다. - -### Phase 4. Pre-Implementation Design Review -- Implementation Issue를 생성하고 설계검토 체크리스트를 작성한다. -- 아키텍처, 인터페이스, 데이터, 오류 처리, 보안/성능, 테스트 전략을 합의한다. -- 미해결 항목은 `Open Items / Assumptions`에 기록한다. - -### Phase 5. Implementation Planning -- `create-implementation-issue.prompt.md`로 구현 이슈를 생성한다. -- `Acceptance Criteria Mapping`으로 AC와 작업 항목을 연결한다. -- AI Agent 작업과 Human 작업을 분리한다. - -### Phase 6. Implementation & PR -- 구현을 진행하고 PR을 생성한다. -- `.github/pull_request_template.md`로 REQ ID, AC 커버리지, 테스트 증거를 기록한다. - -### Phase 7. Verification & Status Transition -- 자동/수동 검증 결과를 반영한다. -- `mark-requirement-implemented.prompt.md`로 `Approved -> Implemented` 전환 가능 여부를 점검한다. -- 충족 여부가 확인되면 requirement 상태를 전환한다. - -### Phase 8. Traceability & Release -- `docs/SRS/traceability-matrix.md`를 갱신한다. -- 릴리즈 노트에 반영된 REQ ID를 기록한다. -- 필요 시 운영 문서/회고 문서에 residual risk를 남긴다. diff --git a/.vendor/myagent-kit/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md b/.vendor/myagent-kit/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md deleted file mode 100644 index 42e8832..0000000 --- a/.vendor/myagent-kit/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: "Implementation From Approved Requirement" -about: "Create an implementation issue from an Approved SRS requirement" -title: "[REQ-XXX] Implement " -labels: ["implementation", "requirement"] -assignees: [] ---- - - - -## 배경 (Context) -- Requirement ID: -- Requirement Link: -- Status Check: Approved - -## 범위 (Scope) -- In Scope: -- Out of Scope: - -## 구현 전 설계검토 체크리스트 (Pre-Implementation Design Review) -- [ ] Architecture approach is documented and agreed -- [ ] Public interface/API contract changes are identified -- [ ] Data model/schema impact is reviewed -- [ ] Error handling and fallback strategy is defined -- [ ] Security/privacy impact is reviewed -- [ ] Performance/scalability impact is reviewed -- [ ] Test strategy (unit/integration/e2e/manual) is defined -- [ ] Rollback or feature-flag strategy is defined when needed -- [ ] Unknowns, assumptions, and decision owners are recorded -- [ ] Human review sign-off completed (or `No additional design review required.`) - -### 설계 결정 요약 (Design Decisions) -- Decision 1: -- Decision 2: - -### 미해결 항목 / 가정 (Open Items / Assumptions) -- - -## AI Agent 작업 (AI Agent Work) -- [ ] Code change task 1 -- [ ] Code change task 2 -- [ ] Automated validation task - -## 사람 작업 (Human Work) -- [ ] Approve scope or open questions -- [ ] Perform required manual verification - -## Acceptance Criteria Mapping -- REQ AC1 -> Task(s): -- REQ AC2 -> Task(s): - -## AI 검증 요약 (AI Validation Summary) -- Automated tests/checks to run: -- Expected evidence: - -## 사람 확인 체크리스트 (Human Verification Checklist) -- [ ] Verify: -- [ ] Confirm: -- If no human verification is required, write: `No manual verification required.` - -## 열린 질문 (Open Questions) -- - -## 리스크 (Risks) -- - -## 완료 조건 (Definition of Done) -- [ ] All mapped acceptance criteria are satisfied -- [ ] Tests are added or updated -- [ ] AI-completable work is finished and evidence is attached -- [ ] Human-only work is completed or explicitly marked not required -- [ ] Docs or change notes are updated if needed diff --git a/.vendor/myagent-kit/base/.github/copilot-instructions.md b/.vendor/myagent-kit/base/.github/copilot-instructions.md deleted file mode 100644 index 6ecd55c..0000000 --- a/.vendor/myagent-kit/base/.github/copilot-instructions.md +++ /dev/null @@ -1,16 +0,0 @@ -# myagent-kit Managed Copilot Instructions - -아래 규칙은 myagent-kit가 관리하는 기본 게이트다. - -## SRS-first Code Change Gate -- 소스 코드 변경이 필요한 요청이면 먼저 `docs/SRS/index.md`와 관련 REQ/SRS 문서를 확인한다. -- SRS와 기술 문서(`docs/technical/`) 기준과 코드 변경 방향이 충돌하면 즉시 사용자에게 선택을 요청한다. -- 사용자가 SRS 변경을 선택하면 `/reopen-requirement-status` 절차를 먼저 수행한다. -- 사용자가 코드 변경 지속을 선택하면 해당 결정을 명시적으로 기록한 뒤 진행한다. - -## No-Conflict Path -- 충돌이 없으면 기술 문서 업데이트가 필요한지 먼저 확인하고, 필요 시 기술 문서를 선반영한 뒤 소스 수정을 진행한다. - -## Traceability -- 코드 변경 시 관련 REQ ID를 확인하고, 가능하면 PR/이슈/테스트 증거와 연결한다. -- 판단이 불충분하면 가정하지 말고 사용자에게 필요한 결정을 질문한다. diff --git a/.vendor/myagent-kit/base/.github/instructions/reopen-requirement-discovery.instructions.md b/.vendor/myagent-kit/base/.github/instructions/reopen-requirement-discovery.instructions.md deleted file mode 100644 index bb579df..0000000 --- a/.vendor/myagent-kit/base/.github/instructions/reopen-requirement-discovery.instructions.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -description: "Use when customizing REQ candidate discovery rules for /reopen-requirement-status command, including keyword dictionary, scoring, tie-breaks, and normalization policy." -applyTo: ".github/prompts/reopen-requirement-status.prompt.md" ---- -# Reopen Requirement Discovery Guidelines - -## 목적 -`/reopen-requirement-status` 입력에 REQ ID가 없을 때, 관련 REQ 후보를 일관된 기준으로 탐색하고 우선순위를 정한다. - -## 후보 탐색 우선순위 -1. 계약 키워드 일치 -- `API`, `endpoint`, `request`, `response`, `schema`, `field`, `CLI`, `command`, `option`, `UI`, `event`, `config`, `env`, `deploy`, `integration` -2. 영향 영역 일치 -- 입력의 변경 대상 모듈/문서/테스트와 REQ의 `Impacted Area` 교집합 -3. 목적/행동 일치 -- 입력의 동사/의도(추가, 제거, 변경, 완화, 강화, 비활성화 등)와 REQ `Intent`/`Requirement`의 의미 유사도 -4. 상태 가중치 -- 현재 상태가 `Approved`, `Implemented`, `Deprecated`인 REQ 우선 - -## 점수 및 신뢰도 -- 점수식: `키워드(0-5) + 영향영역(0-3) + 의미유사(0-2) + 상태가중치(0-1)` -- 정렬: 총점 내림차순 -- 신뢰도: - - `High(7-11)` - - `Medium(4-6)` - - `Low(0-3)` - -## 도메인 키워드 사전 (커스터마이즈 가능) -- Auth & Access: `auth`, `login`, `oauth`, `token`, `permission`, `rbac`, `role`, `scope` -- Billing & Subscription: `billing`, `invoice`, `payment`, `subscription`, `plan`, `quota`, `credit` -- Data & Migration: `schema`, `migration`, `backfill`, `etl`, `import`, `export`, `retention` -- API & Contract: `api`, `endpoint`, `request`, `response`, `versioning`, `webhook`, `payload` -- Runtime & Operations: `deploy`, `rollback`, `release`, `monitoring`, `alert`, `slo`, `incident` -- Configuration: `config`, `env`, `feature flag`, `toggle`, `secret`, `parameter` -- UX & Interface: `ui`, `ux`, `screen`, `form`, `flow`, `command`, `cli`, `option` -- Testing & Quality: `test`, `e2e`, `regression`, `coverage`, `validation`, `acceptance` - -## 정규화 규칙 -- 사전에 없는 표현이 입력에 등장하면 의미가 유사한 사전 키워드로 1회 정규화해 근거에 표기한다. -- 예: `권한` -> `permission`, `과금` -> `billing` - -## 동점 처리 -- 동일 점수 후보가 여러 개면 최신 Change Log 항목이 있는 REQ를 우선한다. -- 여전히 동점이면 현재 active 상태(`Approved`/`Implemented`) REQ를 우선한다. -- 추가 동점이면 REQ ID가 작은 항목을 우선한다. - -## 출력 권장 -- 후보는 최대 5개 제시 -- 각 후보에 `REQ ID`, `신뢰도`, `점수`, `근거 키워드`, `영향 영역`을 포함 -- 확정 전에는 `Immediate Questions`로 최종 대상 REQ 선택을 먼저 요청 diff --git a/.vendor/myagent-kit/base/.github/instructions/requirement-scope-decision.instructions.md b/.vendor/myagent-kit/base/.github/instructions/requirement-scope-decision.instructions.md deleted file mode 100644 index b3df18a..0000000 --- a/.vendor/myagent-kit/base/.github/instructions/requirement-scope-decision.instructions.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -description: "Use when evaluating whether a new feature or change request requires a new or updated SRS requirement, a technical spec update only, or needs owner decision. Apply before starting requirement drafting." -applyTo: "docs/SRS/**/*.md" ---- -# Requirement Scope Decision Guidelines - -## 목적 -새 기능 요청이나 변경 사항이 들어왔을 때, SRS에 등록해야 하는지(신규 또는 기존 REQ 수정) 아니면 technical spec / implementation note만 업데이트하면 되는지를 먼저 판정한다. -이 판정을 거침으로써 불필요한 REQ 생성을 방지하고, SRS에 등록돼야 할 변경이 누락되는 것을 예방한다. - -## 판정 결과 분류 (4종) -1. **New SRS requirement needed** — 새 REQ 문서를 작성해야 한다. -2. **Update existing SRS requirement** — 기존 REQ 문서를 수정해야 한다. -3. **Technical spec only** — SRS 변경 없이 technical spec / implementation note만 업데이트한다. -4. **Needs owner decision** — 운영 리스크나 승인 기준이 불명확하여 소유자(Owner) 결정이 필요하다. - -## SRS 대상 판정 기준 (다음 중 하나라도 해당하면 SRS) - -### 외부 계약 변화 -- 사용자 또는 외부 시스템이 관찰하는 계약이 바뀌는 경우 -- API / CLI / UI / Event / Data schema / Config / Deployment / Integration 요구사항이 변경되는 경우 - -### 측정 가능한 Non-Functional 변경 -- 성능, 보안, 가용성, 확장성 등 측정 기준이 추가되거나 변경되는 경우 - -### Acceptance Criteria 및 승인 게이트 필요 -- 변경 사항에 대해 명시적 수락 기준과 승인 이력이 필요한 경우 - -## Technical Spec Only 판정 기준 (다음에 해당하면 SRS 불필요) -- 내부 리팩터링 (외부 계약 변화 없음) -- 라이브러리 교체 (동일한 외부 인터페이스 유지) -- 코드 구조 / 패턴 변경 (기능 동작 동일) -- 외부 계약을 바꾸지 않는 구현 최적화 - -## Borderline / Needs Owner Decision 판정 기준 -- 운영 리스크나 승인 책임이 불명확한 경우 -- 제품 의도가 불분명하거나 범위가 애매한 경우 -- 위 기준만으로 분류가 확정되지 않는 경우 - -## New vs. Update 구분 기준 -- 동일 기능/계약 범위의 변경 → `Update existing SRS requirement` -- 새 기능/계약/인터페이스의 추가 → `New SRS requirement needed` -- 기존 REQ 검색 시 `docs/SRS/index.md`의 Requirement Register를 먼저 확인한다. - -## 판정 출력 형식 -판정 결과는 항상 다음 항목을 포함해야 한다. -1. **Decision** — 위 4종 중 하나 -2. **Why** — 판정 근거 (구체적으로 어떤 기준에 해당하는지) -3. **Recommended Type** — SRS 대상인 경우, 해당 requirement type (Interface / Data / Deployment 등) -4. **Existing REQ candidates** — Update 가능성이 있는 기존 REQ ID 목록 (해당 시) -5. **Next Action** — 판정 결과에 따른 다음 단계 안내 -6. **Open Questions** — 추가 확인이 필요한 사항 (없으면 `없음`) diff --git a/.vendor/myagent-kit/base/.github/instructions/srs.instructions.md b/.vendor/myagent-kit/base/.github/instructions/srs.instructions.md deleted file mode 100644 index d2e3533..0000000 --- a/.vendor/myagent-kit/base/.github/instructions/srs.instructions.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -description: "Use when writing or editing SRS documents, requirement files, acceptance criteria, change logs, or impacted area entries under docs/SRS. Ensures requirement quality, traceability, and testable wording." -applyTo: "docs/SRS/**/*.md" ---- -# SRS Document Guidelines - -## Requirement 타입 분류 - -### 분류 의사결정 규칙 (우선순위 순) -1. **Installation**: "시스템 설치, 환경 구성, 초기화 절차를 정의하는가?" → YES면 Installation - - 예: Docker 이미지 빌드, 데이터베이스 마이그레이션, 사전 조건 설정 -2. **Interface**: "사용자가 직접 조작하거나 실행하는 방식을 정의하는가?" → YES면 Interface - - 예: CLI 명령어, API 엔드포인트, 웹 UI 기능, 제어 방식 -3. **Configuration**: "시스템 또는 기능의 설정 옵션, 커스터마이징을 정의하는가?" → YES면 Configuration - - 예: 설정 파라미터, 옵션 항목, 환경 변수, 커스터마이징 포인트 -4. **Data**: "데이터 저장, 로드, 구조, 파일 형식, 스키마를 정의하는가?" → YES면 Data - - 예: 설정 파일 형식, 데이터베이스 스키마, API 요청/응답 구조 -5. **Exception & Error Handling**: "오류 처리, 예외 복구, 재시도 전략을 중심으로 하는가?" → YES면 Exception & Error Handling - - 예: 네트워크 오류 재시도, 데이터 검증 실패 처리, 복구 절차 -6. **Deployment**: "배포 방식, 운영 절차, 배포 환경을 정의하는가?" → YES면 Deployment - - 예: 컨테이너 배포, 스택 관리, 운영 가이드 -7. **Documentation**: "문서 작성, 가이드, 매뉴얼, 안내서를 정의하는가?" → YES면 Documentation - - 예: 사용자 가이드, API 문서, 운영 매뉴얼, 튜토리얼 -8. **Integration**: "외부 시스템 연동, 플러그인, 확장 기능을 정의하는가?" → YES면 Integration - - 예: 3rd party 서비스 연동, 플러그인 인터페이스, 확장 메커니즘 -9. **Migration**: "데이터 이전, 버전 업그레이드, 마이그레이션을 정의하는가?" → YES면 Migration - - 예: 데이터 마이그레이션, 버전 호환성, 업그레이드 절차 -10. **Testing**: "테스트 전략, QA 요구사항, 검증 방식을 정의하는가?" → YES면 Testing - - 예: 테스트 커버리지, 회귀 테스트, 수락 테스트 기준 -11. **Non-Functional**: 위 모든 경우에 해당하지 않으면 → Non-Functional - - 예: 성능 목표, 보안 정책, 확장성, 호환성, 신뢰성 요구사항 - -**주의**: requirement가 여러 분류에 해당하면 주된 제약 또는 목적에 따라 하나로만 선택한다. 예를 들어: -- "Docker Compose로 배포할 수 있어야 한다" → Deployment -- "배포 시 환경 변수로 설정 가능해야 한다" → Configuration -- "배포 전에 다음 점검 항목을 검증해야 한다" → Testing/Documentation - -- `docs/SRS/index.md`는 SRS 운영 기준과 Requirement Register를 관리하는 문서로 유지한다. -- `docs/SRS/REQ-XXX_*.md`는 하나의 requirement만 다루고, 다른 requirement 내용을 섞지 않는다. 파일은 Type별 폴더에 저장한다. 예: `docs/SRS/Deployment/REQ-001_docker-compose-release.md` -- 새 requirement 번호의 source of truth는 `docs/SRS/index.md`의 `Next Requirement ID`다. -- requirement 본문에는 최소한 `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log`가 포함되어야 한다. -- `Acceptance Criteria`는 관찰 가능하고 test 가능한 문장으로 작성한다. -- 모호한 표현("개선한다", "적절히 처리한다", "최적화한다")은 측정 기준 없이 사용하지 않는다. -- requirement를 수정하면 해당 REQ 파일의 `Change Log`도 함께 갱신한다. -- `Impacted Area`는 `Docs`, `Modules`, `Tests`를 모두 검토한 뒤 작성한다. 해당 항목이 없으면 `없음`으로 명시한다. -- implementation issue/PR를 생성하거나 병합할 때는 `docs/SRS/traceability-matrix.md`에서 해당 REQ 행의 `Issue/PR`, `Tests`, `Status`를 갱신한다. -- 새로운 Type으로 requirement를 추가할 때는 `docs/SRS//` 폴더 하위에 저장한다. 아직 없는 Type 폴더면 생성한다. -- 새로운 requirement를 추가하면 `docs/SRS/index.md`의 Requirement Register에도 같은 ID를 추가한다. -- 새로운 requirement를 추가하거나 Register가 바뀌면 `docs/SRS/index.md`의 `Next Requirement ID`도 함께 갱신한다. -- `docs/SRS/index.md`의 `Recent Change Log Summary`는 최신 5건만 유지하고 최신 항목을 위에 둔다. -- requirement ID와 파일명 prefix는 일치해야 한다. 예: `REQ-002`는 `docs/SRS//REQ-002_.md`. -- requirement 상태를 `Approved` 또는 `Implemented`로 바꿀 때는 acceptance criteria와 change history가 누락되지 않았는지 먼저 검토한다. -- requirement 상태를 전환할 때는 `docs/SRS/index.md`의 `Status Transition Evidence` 기준을 따른다. diff --git a/.vendor/myagent-kit/base/.github/prompts/approve-requirement.prompt.md b/.vendor/myagent-kit/base/.github/prompts/approve-requirement.prompt.md deleted file mode 100644 index 3bf3267..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/approve-requirement.prompt.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -description: "Use when: decide whether a Proposed requirement is ready to move to Approved and generate the required status-change edits" -name: "Approve Requirement" -argument-hint: "Paste a Proposed REQ file or select requirement text" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서가 `Proposed`에서 `Approved`로 전환 가능한지 검토한다. - -입력: -${input} - -검토 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 의 `Status Transition Evidence`와 해당 requirement 문서 구조다. -- `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log` 존재 여부를 검토한다. -- Type이 SRS index의 분류 도표에 정의된 타입 중 정확하게 하나로 지정되었는지 확인한다. -- `Acceptance Criteria`가 구현 기준으로 충분히 구체적인지 판단한다. -- 중요한 assumption 또는 open question이 남아 있으면 승인 blocker로 본다. -- 승인 가능하면 status 변경과 Change Log 추가 초안을 함께 제안한다. -- 승인 불가하면 blocker를 우선순위 순으로 정리한다. - -출력 형식: -1. Approval Decision: `Approved-ready` 또는 `Not ready` -2. Blockers -3. Suggested Edits -4. Approval Change Log Draft diff --git a/.vendor/myagent-kit/base/.github/prompts/complete-requirement.prompt.md b/.vendor/myagent-kit/base/.github/prompts/complete-requirement.prompt.md deleted file mode 100644 index ea0de9a..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/complete-requirement.prompt.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -description: "Use when: complete a partial SRS requirement draft into a full REQ file with acceptance criteria and impacted area" -name: "Complete Requirement Draft" -argument-hint: "Paste a partial requirement draft or describe the feature" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기반으로 SRS requirement 초안을 완성한다. - -입력: -${input} - -작업 규칙: -- SRS 기준 문서는 [SRS index](../../docs/SRS/index.md) 와 관련 [REQ template](../../docs/SRS/REQ-template.md) 를 따른다. -- 입력에 `REQ-XXX` 형식의 명시적 ID가 없으면, [SRS index](../../docs/SRS/index.md)의 `Next Requirement ID`를 우선 읽어 그 값을 사용한다. `Next Requirement ID`가 없거나 신뢰할 수 없으면 `Requirement Register`와 기존 REQ 파일들 기준의 최고 번호에 1을 더한 값을 사용한다. -- 사용자가 이미 특정 REQ ID를 명시했으면 그 ID를 유지하고 임의로 바꾸지 않는다. -- requirement의 유형을 인식하고 `Type` 필드를 정확하게 지정한다. 분류는 [SRS index](../../docs/SRS/index.md)의 "Requirement 작성 규칙" 또는 [SRS instructions](../../.github/instructions/srs.instructions.md)의 "분류 의사결정 규칙"을 따른다. -- 최종 `Type`과 `REQ ID`를 이용해 추천 파일 경로를 함께 제시한다. 형식은 `docs/SRS//REQ-XXX_.md`를 사용한다. -- ``는 requirement 제목을 기반으로 소문자 kebab-case 영문으로 생성한다. 공백은 `-`로 치환하고, 영문/숫자/`-`만 남긴다. -- 제목이 한글이거나 slug 생성이 불명확하면 의미를 유지하는 짧은 영문 slug를 보수적으로 제안한다. -- 사용자가 제공한 의도와 핵심 동작을 보존한다. -- 빠진 항목은 보수적으로 보완하되, 추정이 필요한 내용은 `Notes`에 assumption으로 분리한다. -- `Acceptance Criteria`는 반드시 test 가능하게 작성한다. -- `Impacted Area`는 Docs, Modules, Tests로 구분한다. -- 새로 할당한 ID를 문서 제목 `# REQ-XXX: ...`에 반영한다. -- 결과는 REQ 파일에 바로 붙여넣을 수 있는 Markdown 형식으로만 출력한다. -- 코드 구현 계획은 포함하지 않는다. - -출력 형식: -1. `Recommended File Path: docs/SRS//REQ-XXX_.md` 한 줄 -2. 실제 할당된 ID가 반영된 `# REQ-XXX: ...` 형식의 전체 문서 본문 -3. `## Change Log` 아래에 초안 생성용 `CHG-YYYYMMDD-01` 항목 1개 포함 diff --git a/.vendor/myagent-kit/base/.github/prompts/create-implementation-issue.prompt.md b/.vendor/myagent-kit/base/.github/prompts/create-implementation-issue.prompt.md deleted file mode 100644 index 1566340..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/create-implementation-issue.prompt.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -description: "Use when: create an implementation plan and GitHub issue draft from an Approved requirement" -name: "Create Implementation Issue" -argument-hint: "Paste an Approved REQ file" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 `Approved` requirement 문서를 기반으로 implementation issue 초안을 생성한다. - -입력: -${input} - -작업 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 와 해당 REQ 문서다. -- 출력 언어는 기본적으로 한국어로 작성한다. -- section 제목, 설명, 작업 지시문은 한국어를 우선 사용하되, 중요한 technical term는 영어를 유지하거나 한국어 뒤 괄호로 병기한다. 예: `AI Agent Work`, `Human Verification Checklist`, `Acceptance Criteria`, `Open Questions`, `Risk`, `Definition of Done`. -- 코드 식별자, 파일 경로, 명령어, API 이름, 테스트 이름, UI 라벨 등 구현 대상과 직접 연결되는 literal은 영어 원문을 유지한다. -- requirement의 `Acceptance Criteria`를 구현 task로 분해한다. -- task는 구현 단위로 나누고, 각 task를 `AI Agent가 완료 가능한 작업`과 `Human이 수행해야 하는 작업`으로 구분한다. -- test task를 반드시 포함한다. -- 각 acceptance criteria 또는 task에 대해 다음을 판단한다: - - AI Agent가 코드 변경과 자동 검증까지 완료할 수 있는가 - - Human이 직접 확인해야 하는 수동 검증이 남는가 -- Human이 직접 확인해야 하는 항목이 있으면 `Human Verification Checklist`에 구체적인 확인 절차와 기대 결과를 작성한다. -- AI Agent가 자체적으로 테스트를 완료할 수 있어 Human 확인이 불필요하면 `Human Verification Checklist`에 `No manual verification required.`를 명시하고 그 근거를 `AI Validation Summary`에 적는다. -- `Impacted Area`를 기반으로 module/docs/test 변경 범위를 제시한다. -- 미해결 가정이나 리스크가 있으면 issue 본문에 `Open Questions`와 `Risks`로 명시한다. -- 출력은 GitHub issue에 바로 붙여 넣을 수 있는 Markdown 형식으로만 작성한다. - -출력 형식: -1. Issue Title -2. 배경 (Context) -3. 범위 (Scope) -4. AI Agent 작업 (AI Agent Work) 체크리스트 -5. 사람 작업 (Human Work) 체크리스트 -6. Acceptance Criteria Mapping (REQ 기준) -7. AI 검증 요약 (AI Validation Summary) -8. 사람 확인 체크리스트 (Human Verification Checklist) -9. 열린 질문 (Open Questions) -10. 리스크 (Risks) -11. 완료 조건 (Definition of Done) -12. GitHub Issue Create Command (`gh issue create`) 예시 diff --git a/.vendor/myagent-kit/base/.github/prompts/decide-requirement-scope.prompt.md b/.vendor/myagent-kit/base/.github/prompts/decide-requirement-scope.prompt.md deleted file mode 100644 index 4a847ec..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/decide-requirement-scope.prompt.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -description: "Use when: evaluate whether a new feature or change request needs a new SRS requirement, an update to an existing one, a technical spec update only, or owner decision — before starting requirement drafting" -name: "Decide Requirement Scope" -argument-hint: "Describe the feature or change request: include feature description, issue body, and/or PR/diff summary" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기반으로, 이 변경이 SRS 대상인지 아닌지를 판정한다. - -입력: -${input} - -판정 규칙: -- 기준 문서는 [requirement-scope-decision instructions](../../.github/instructions/requirement-scope-decision.instructions.md) 와 [SRS index](../../docs/SRS/index.md) 의 Requirement Register다. -- 다음 4가지 결과 중 하나를 반드시 반환한다. - 1. `New SRS requirement needed` - 2. `Update existing SRS requirement` - 3. `Technical spec only` - 4. `Needs owner decision` -- 판정 기준을 순서대로 적용한다. - - 사용자/외부 시스템이 관찰하는 계약(API, CLI, UI, Event, Data schema, Config, Deployment, Integration)이 바뀌는가? → YES면 SRS - - 측정 가능한 Non-Functional requirement(성능, 보안, 가용성 등)가 추가/변경되는가? → YES면 SRS - - Acceptance Criteria 및 명시적 승인 게이트가 필요한가? → YES면 SRS - - 위 기준에 모두 해당하지 않으면서 내부 리팩터링·라이브러리 교체·코드 구조 변경·구현 최적화인가? → Technical spec only - - 위 기준만으로 분류가 확정되지 않거나 운영 책임/리스크가 불명확한가? → Needs owner decision -- SRS 대상으로 판정되면, 신규 기능/계약인지 기존 REQ 범위 내 변경인지를 구분한다. - - 기존 REQ에 해당하는 경우 [SRS index](../../docs/SRS/index.md) 의 Requirement Register에서 후보 REQ ID를 찾아 제시한다. -- Technical spec only로 판정되면, 불필요한 REQ 생성을 명시적으로 피하도록 안내한다. -- 결과에는 항상 `Why`와 `Next Action`을 포함한다. -- `Open Questions`가 없으면 `없음`으로 명시한다. - -출력 형식: -1. **Decision**: `New SRS requirement needed` / `Update existing SRS requirement` / `Technical spec only` / `Needs owner decision` 중 하나 -2. **Why**: 판정 근거 — 어떤 기준에 해당하는지 구체적으로 설명 -3. **Recommended Type**: (SRS 대상인 경우만) Interface / Data / Deployment / Configuration 등 해당 requirement type -4. **Existing REQ candidates**: (Update 가능성이 있는 경우만) 관련 REQ ID와 제목 목록. 해당 없으면 생략 -5. **Next Action**: 판정 결과에 따른 다음 단계 안내 -6. **Open Questions**: 추가 확인이 필요한 사항. 없으면 `없음` diff --git a/.vendor/myagent-kit/base/.github/prompts/mark-requirement-implemented.prompt.md b/.vendor/myagent-kit/base/.github/prompts/mark-requirement-implemented.prompt.md deleted file mode 100644 index b1567f5..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/mark-requirement-implemented.prompt.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -description: "Use when: decide whether an Approved requirement is ready to move to Implemented and generate the required status-change edits" -name: "Mark Requirement Implemented" -argument-hint: "Paste an Approved REQ file and implementation evidence" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서와 implementation evidence를 검토해 `Approved`에서 `Implemented`로 전환 가능한지 판단한다. - -입력: -${input} - -검토 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 의 `Status Transition Evidence`와 해당 requirement 문서 구조다. -- requirement의 `Acceptance Criteria`가 implementation evidence, tests, 또는 manual verification으로 충족되었는지 확인한다. -- 충족되지 않은 acceptance criteria가 있으면 구현 완료로 판정하지 않는다. -- 구현 완료 가능하면 status 변경, 잔여 리스크, Change Log 추가 초안을 함께 제안한다. -- 구현 완료 불가하면 미충족 기준과 필요한 검증 항목을 정리한다. - -출력 형식: -1. Implementation Decision: `Implemented-ready` 또는 `Not ready` -2. Unverified Criteria -3. Suggested Edits -4. Implementation Change Log Draft -5. Residual Risk diff --git a/.vendor/myagent-kit/base/.github/prompts/reopen-requirement-status.prompt.md b/.vendor/myagent-kit/base/.github/prompts/reopen-requirement-status.prompt.md deleted file mode 100644 index 695c628..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/reopen-requirement-status.prompt.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -description: "Use when: move Approved, Implemented, or Deprecated requirements back to Proposed (or Re-review) with decision checkpoints, conflict candidates, and fallback REQ discovery" -name: "Reopen Requirement Status" -argument-hint: "(선택) REQ-XXX, REQ-XXX; 변경 의도와 범위 설명" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기준으로 지정된 requirement를 재검토 상태로 되돌리는 변경안을 만든다. - -입력: -${input} - -입력 해석 규칙: -- 입력 형식은 기본적으로 `REQ-XXX, REQ-XXX; 이렇게 이렇게 변경할거야.` 를 따른다. -- `;` 앞은 대상 REQ ID 목록, 뒤는 변경 의도/배경으로 해석한다. -- 구분자는 쉼표/공백을 모두 허용하되, 최종적으로 중복 없는 REQ ID 목록으로 정규화한다. -- 입력에 REQ ID가 없으면 전체 입력을 변경 의도/배경으로 간주한다. - -작업 규칙: -- 기준 문서는 `docs/SRS/index.md`, 해당 REQ 파일, [SRS instructions](../../.github/instructions/srs.instructions.md), [Reopen requirement discovery instructions](../../.github/instructions/reopen-requirement-discovery.instructions.md) 다. -- 대상 REQ ID가 명시되지 않으면 `docs/SRS/index.md` Requirement Register와 REQ 파일 제목/Intent/Requirement/Impacted Area를 기반으로 관련 후보를 탐색한다. -- 후보 탐색, 점수화, 신뢰도, 동점 처리, 도메인 키워드 정규화는 [Reopen requirement discovery instructions](../../.github/instructions/reopen-requirement-discovery.instructions.md)를 그대로 따른다. -- REQ 미지정 시 후보는 최대 5개까지 제시하고, 신뢰도(High/Medium/Low)와 근거(키워드/영향 영역)를 함께 제시한다. -- REQ 미지정 시 확정 전에는 상태 변경안을 단정하지 말고, `Immediate Questions`에서 최종 대상 REQ 확인 질문을 먼저 제시한다. -- 사용자가 후보 중 선택하면 선택된 REQ 기준으로 상태 되돌림 변경안을 완성한다. -- 대상 REQ의 현재 상태가 `Approved`, `Implemented`, `Deprecated` 중 하나인지 확인한다. -- 상태 되돌림의 목표 상태는 기본 `Proposed`로 제안한다. -- 팀이 `재검토 상태`를 별도로 운용하면 목표 상태를 `Re-review`(또는 팀 표기)로 제안할 수 있다. 단, 최종 상태값은 사용자 결정 항목으로 분리한다. -- 각 REQ마다 왜 되돌려야 하는지(계약 변경, acceptance criteria 변경, 리스크 발견, 구현 불일치 등) 근거를 1-2줄로 요약한다. -- 변경 전/후 영향으로 충돌 가능성이 있는 REQ 후보를 `docs/SRS/index.md` Requirement Register 기준으로 제시한다. -- 충돌 후보는 다음 기준으로 찾는다: 동일 기능 영역, 동일 인터페이스/API/CLI/데이터 스키마, 같은 모듈/문서 영향 범위, 기존 acceptance criteria 중복/상충. -- 사용자가 결정해야 할 항목을 별도 섹션으로 분리한다. -- 결정 누락 시 즉시 물어야 할 질문을 먼저 제시한다. -- 실제 문서 변경을 수행하는 대신, 바로 적용 가능한 수정안(상태, Change Log, impacted area 재검토 포인트)을 제시한다. - -출력 형식: -1. Reopen Decision -2. Immediate Questions (없으면 `없음`) -3. Detected REQ Candidates (REQ 미지정인 경우 필수, 지정된 경우 생략 가능) -4. User Decisions Required -5. Suggested Status Changes (REQ별: `from -> to`) -6. Conflict Candidate REQs (REQ별 후보 ID와 충돌 이유) -7. Suggested REQ Edits (Status, Intent/Requirement/Acceptance Criteria 영향, Change Log 초안) -8. Next Action diff --git a/.vendor/myagent-kit/base/.github/prompts/sync-srs-index.prompt.md b/.vendor/myagent-kit/base/.github/prompts/sync-srs-index.prompt.md deleted file mode 100644 index e8ecc03..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/sync-srs-index.prompt.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -description: "Use when: update docs/SRS/index.md Requirement Register or Recent Change Log Summary after adding or editing REQ files" -name: "Sync SRS Index" -argument-hint: "Paste changed REQ files or describe what changed in SRS" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기준으로 `docs/SRS/index.md`를 갱신하기 위한 제안을 만든다. - -입력: -${input} - -작업 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 이다. -- 새 requirement가 추가되었거나 Register가 바뀌면 `Requirement Register`의 최고 번호를 기준으로 다음 번호를 계산해 `Next Requirement ID`도 함께 갱신한다. -- 새 requirement가 추가되었으면 `Requirement Register`에 들어갈 행을 만든다. -- 기존 requirement가 수정되었으면 `Recent Change Log Summary`에 들어갈 요약 항목을 만든다. -- 기존 구조를 바꾸지 말고, 필요한 행과 요약만 제안한다. -- `Next Requirement ID`는 `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 큰 값이어야 한다. -- `Recent Change Log Summary`는 최신 변경을 위에 두고, 짧은 요약 1줄로 작성한다. -- `Recent Change Log Summary`는 최신 5건만 유지하도록 제안한다. 5건을 초과하면 제거 또는 대체해야 할 가장 오래된 항목도 함께 설명한다. -- 출력은 사람이 `docs/SRS/index.md`에 바로 반영할 수 있는 Markdown 조각으로만 작성한다. - -출력 형식: -1. `Next Requirement ID`에 반영할 Markdown bullet -2. `Requirement Register`에 추가 또는 수정할 Markdown table row -3. `Recent Change Log Summary`에 추가할 Markdown section -4. 필요한 경우 only, index에서 제거 또는 갱신해야 할 기존 항목 설명 diff --git a/.vendor/myagent-kit/base/.github/prompts/sync-technical-specs.prompt.md b/.vendor/myagent-kit/base/.github/prompts/sync-technical-specs.prompt.md deleted file mode 100644 index 3cdb1c6..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/sync-technical-specs.prompt.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -description: "Use when: verify one or more REQ IDs are Approved and create or update the matching docs/technical specifications" -name: "Sync Technical Specs" -argument-hint: "Enter REQ IDs, e.g. REQ-011, REQ-007, REQ-012" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력으로 지정된 requirement ID 목록에 대해, 승인 상태를 확인하고 해당하는 `docs/technical/` 문서를 실제로 갱신한다. - -입력: -${input} - -작업 규칙: -- 입력에서 `REQ-XXX` 목록을 추출한다. 쉼표 또는 공백으로 구분된 여러 ID를 지원한다. -- 먼저 `docs/SRS/index.md`의 `Requirement Register`와 각 REQ 파일의 `Status`를 함께 확인한다. -- 두 곳 모두 `Approved`인 requirement만 기술명세 업데이트 대상으로 포함한다. -- 하나라도 `Approved`가 아니거나 파일을 찾지 못하면 해당 REQ는 blocker로 보고하고, 그 REQ에 대한 technical doc은 수정하지 않는다. -- `docs/technical/index.md`와 각 technical doc 상단의 `Related REQ`를 기준으로 기존 연결 문서를 찾는다. -- 연결 문서가 있으면 현재 승인된 requirement에 맞게 내용을 실제로 업데이트한다. -- 연결 문서가 없으면 `docs/technical/` 아래에 새 문서를 생성하고 `docs/technical/index.md`에 항목을 추가한다. -- 기술 문서는 SRS를 source of truth로 따르며, 외부 계약이나 acceptance criteria를 재정의하지 않는다. 대신 구현 상세(모듈 책임, 파일 위치, 설정/환경값, 런타임 흐름, validation notes, open implementation points)를 정리한다. -- 문서 상단에는 반드시 `- Related REQ: ...`를 유지하거나 추가한다. -- `REQ-011`, `REQ-007`, `REQ-012`는 다음 문서를 우선 후보로 사용한다: - - `REQ-011` → `docs/technical/project-structure-baseline.md` - - `REQ-007` → `docs/technical/python-runtime-and-packaging.md` - - `REQ-012` → `docs/technical/settings-and-env-loading.md` -- `docs/technical/index.md`의 문서 목록과 연관 REQ 표도 문서 생성/연결 변경 사항에 맞게 함께 갱신한다. -- 실제 파일 변경을 수행한 뒤, 변경된 파일과 각 REQ의 승인 확인 결과를 요약한다. - -출력 형식: -1. Approval Check -2. Technical Spec Updates -3. Files Changed -4. Remaining Blockers or Notes diff --git a/.vendor/myagent-kit/base/.github/prompts/validate-requirement.prompt.md b/.vendor/myagent-kit/base/.github/prompts/validate-requirement.prompt.md deleted file mode 100644 index bbb9ba1..0000000 --- a/.vendor/myagent-kit/base/.github/prompts/validate-requirement.prompt.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -description: "Use when: review an SRS requirement for ambiguity, missing acceptance criteria, missing impacted area, and change-log gaps" -name: "Validate Requirement Quality" -argument-hint: "Paste a REQ file or select requirement text" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서를 검토한다. - -입력: -${input} - -검토 규칙: -- 기준은 [SRS index](../../docs/SRS/index.md) 와 관련 requirement 문서 구조를 따른다. -- 다음 항목을 우선 검토한다: 모호한 표현, test 불가능한 문장, 누락된 acceptance criteria, 누락된 impacted area, 상태/유형/우선순위/owner 누락, Type이 의사결정 규칙에 따라 정확하게 지정되었는지, Change Log 누락 또는 불일치. - - Type 분류 규칙: 사용자 조작 방식 → Interface, 데이터 저장/구조 → Data, 오류 처리 → Exception & Error Handling, 그 외 → Non-Functional -- 중요한 문제부터 우선순위 순으로 정리한다. -- 문제가 없으면 `No critical findings.` 로 시작한다. -- requirement 본문을 직접 다시 작성하지 말고, 수정 제안 위주로 답한다. - -출력 형식: -1. Findings -2. Open Questions -3. Suggested Edits diff --git a/.vendor/myagent-kit/base/.github/pull_request_template.md b/.vendor/myagent-kit/base/.github/pull_request_template.md deleted file mode 100644 index 4af7096..0000000 --- a/.vendor/myagent-kit/base/.github/pull_request_template.md +++ /dev/null @@ -1,24 +0,0 @@ -## Requirement Traceability - -- Requirement ID: REQ-XXX -- Requirement Link: docs/SRS//REQ-XXX_.md -- Related Issue: # - -## Acceptance Criteria Coverage -- [ ] AC1: -- [ ] AC2: - -## Test Evidence -- Automated: - - -- Manual: - - - - If not required: `No manual verification required.` - -## Impacted Area Check -- Docs: -- Modules: -- Tests: - -## Release Notes Input -- [ ] Added REQ ID to release notes (or tracked release note draft) diff --git a/.vendor/myagent-kit/bootstrap/install.sh b/.vendor/myagent-kit/bootstrap/install.sh deleted file mode 100755 index 2cf7999..0000000 --- a/.vendor/myagent-kit/bootstrap/install.sh +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -KIT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -MERGE_COPILOT_INSTRUCTIONS=0 -DRY_RUN=0 -PROJECT_ROOT="" - -usage() { - cat <<'EOF' -Usage: bootstrap/install.sh [PROJECT_ROOT] [--merge-copilot-instructions] [--dry-run] - -Options: - --merge-copilot-instructions Opt-in: safely merge .github/copilot-instructions.md - --dry-run Print planned changes without modifying files - -h, --help Show this help message -EOF -} - -log_action() { - local message="$1" - if [[ "$DRY_RUN" -eq 1 ]]; then - echo "[dry-run] $message" - else - echo "$message" - fi -} - -run_or_dry() { - if [[ "$DRY_RUN" -eq 1 ]]; then - log_action "$*" - return 0 - fi - "$@" -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --merge-copilot-instructions) - MERGE_COPILOT_INSTRUCTIONS=1 - shift - ;; - --dry-run) - DRY_RUN=1 - shift - ;; - -h|--help) - usage - exit 0 - ;; - -*) - echo "Unknown option: $1" >&2 - usage >&2 - exit 1 - ;; - *) - if [[ -z "$PROJECT_ROOT" ]]; then - PROJECT_ROOT="$1" - shift - else - echo "Unexpected extra argument: $1" >&2 - usage >&2 - exit 1 - fi - ;; - esac -done - -PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}" - -MANAGED_BEGIN='' -MANAGED_END='' -PROJECT_BEGIN='' -PROJECT_END='' - -count_occurrence() { - local needle="$1" - local file="$2" - grep -cF "$needle" "$file" || true -} - -build_managed_block() { - local kit_file="$1" - { - printf '%s\n' "$MANAGED_BEGIN" - cat "$kit_file" - printf '\n%s\n' "$MANAGED_END" - } -} - -append_project_section_if_missing() { - local file="$1" - local project_begin_count - local project_end_count - - project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$file")" - project_end_count="$(count_occurrence "$PROJECT_END" "$file")" - - if [[ "$project_begin_count" -eq 0 && "$project_end_count" -eq 0 ]]; then - cat >> "$file" < -$PROJECT_END -EOF - elif [[ "$project_begin_count" -ne "$project_end_count" ]]; then - return 1 - fi -} - -validate_merged_copilot_file() { - local file="$1" - local managed_begin_count - local managed_end_count - local project_begin_count - local project_end_count - - managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$file")" - managed_end_count="$(count_occurrence "$MANAGED_END" "$file")" - project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$file")" - project_end_count="$(count_occurrence "$PROJECT_END" "$file")" - - [[ "$managed_begin_count" -eq 1 ]] || return 1 - [[ "$managed_end_count" -eq 1 ]] || return 1 - [[ "$project_begin_count" -eq 1 ]] || return 1 - [[ "$project_end_count" -eq 1 ]] || return 1 -} - -merge_copilot_instructions() { - local kit_file="$KIT_ROOT/base/.github/copilot-instructions.md" - local host_file="$PROJECT_ROOT/.github/copilot-instructions.md" - local backup_file="$host_file.bak" - local tmp_file - local managed_block - local managed_begin_count - local managed_end_count - - if [[ "$MERGE_COPILOT_INSTRUCTIONS" -ne 1 ]]; then - return 0 - fi - - if [[ ! -f "$kit_file" ]]; then - log_action "Skipped Copilot merge: kit template not found at $kit_file" - return 0 - fi - - if [[ "$DRY_RUN" -eq 1 ]]; then - if [[ ! -f "$host_file" ]]; then - log_action "Would create and initialize $host_file with managed + project-specific sections" - return 0 - fi - - managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$host_file")" - managed_end_count="$(count_occurrence "$MANAGED_END" "$host_file")" - - log_action "Would back up $host_file to $host_file.bak" - if [[ "$managed_begin_count" -eq 1 && "$managed_end_count" -eq 1 ]]; then - log_action "Would update managed myagent-kit section in $host_file" - elif [[ "$managed_begin_count" -eq 0 && "$managed_end_count" -eq 0 ]]; then - log_action "Would append managed myagent-kit section to $host_file" - else - log_action "Would fail: malformed managed marker pair in $host_file" - return 1 - fi - - local project_begin_count - local project_end_count - project_begin_count="$(count_occurrence "$PROJECT_BEGIN" "$host_file")" - project_end_count="$(count_occurrence "$PROJECT_END" "$host_file")" - if [[ "$project_begin_count" -eq 0 && "$project_end_count" -eq 0 ]]; then - log_action "Would append project-specific editable section to $host_file" - elif [[ "$project_begin_count" -ne "$project_end_count" ]]; then - log_action "Would fail: malformed project-specific marker pair in $host_file" - return 1 - fi - return 0 - fi - - managed_block="$(build_managed_block "$kit_file")" - tmp_file="$(mktemp)" - - if [[ ! -f "$host_file" ]]; then - { - printf '%s\n' "$managed_block" - printf '\n%s\n' "$PROJECT_BEGIN" - printf '\n' - printf '%s\n' "$PROJECT_END" - } > "$host_file" - validate_merged_copilot_file "$host_file" - log_action "Created and initialized $host_file" - rm -f "$tmp_file" - return 0 - fi - - cp "$host_file" "$backup_file" - managed_begin_count="$(count_occurrence "$MANAGED_BEGIN" "$host_file")" - managed_end_count="$(count_occurrence "$MANAGED_END" "$host_file")" - - if [[ "$managed_begin_count" -eq 1 && "$managed_end_count" -eq 1 ]]; then - awk \ - -v begin="$MANAGED_BEGIN" \ - -v end="$MANAGED_END" \ - -v block="$managed_block" ' - BEGIN { - in_block = 0 - replaced = 0 - } - $0 == begin { - if (!replaced) { - print block - replaced = 1 - } - in_block = 1 - next - } - $0 == end { - in_block = 0 - next - } - !in_block { - print - } - END { - if (!replaced) { - print "" - print block - } - } - ' "$host_file" > "$tmp_file" - elif [[ "$managed_begin_count" -eq 0 && "$managed_end_count" -eq 0 ]]; then - { - cat "$host_file" - printf '\n%s\n' "$managed_block" - } > "$tmp_file" - else - echo "Malformed managed marker pair in $host_file" >&2 - cp "$backup_file" "$host_file" - rm -f "$tmp_file" - return 1 - fi - - mv "$tmp_file" "$host_file" - append_project_section_if_missing "$host_file" - validate_merged_copilot_file "$host_file" - log_action "Merged $host_file (backup: $backup_file)" -} - -run_or_dry mkdir -p "$PROJECT_ROOT/.github/prompts" \ - "$PROJECT_ROOT/.github/instructions" \ - "$PROJECT_ROOT/.github/ISSUE_TEMPLATE" - -run_or_dry cp "$KIT_ROOT/base/.github/prompts/"*.prompt.md "$PROJECT_ROOT/.github/prompts/" -run_or_dry cp "$KIT_ROOT/base/.github/instructions/"*.instructions.md "$PROJECT_ROOT/.github/instructions/" -run_or_dry cp "$KIT_ROOT/base/.github/ISSUE_TEMPLATE/"*.md "$PROJECT_ROOT/.github/ISSUE_TEMPLATE/" -if [[ -f "$KIT_ROOT/base/.github/pull_request_template.md" ]]; then - run_or_dry cp "$KIT_ROOT/base/.github/pull_request_template.md" "$PROJECT_ROOT/.github/pull_request_template.md" -fi - -merge_copilot_instructions - -log_action "Installed myagent-kit assets into $PROJECT_ROOT/.github" diff --git a/.vendor/myagent-kit/templates/docs/SRS/REQ-template.md b/.vendor/myagent-kit/templates/docs/SRS/REQ-template.md deleted file mode 100644 index 863168c..0000000 --- a/.vendor/myagent-kit/templates/docs/SRS/REQ-template.md +++ /dev/null @@ -1,55 +0,0 @@ -# REQ-XXX: <요구사항 제목> - -- Status: Proposed | Approved | Implemented | Deprecated -- Type: Interface | Data | Exception & Error Handling | Installation | Configuration | Deployment | Documentation | Integration | Testing | Non-Functional -- Priority: High | Medium | Low -- Owner: <담당자 또는 팀> - -## Intent -- <이 요구사항이 필요한 이유와 달성하려는 목표를 1~3문장으로 작성> - -## Requirement -- <시스템이 반드시 수행해야 하는 동작을 단정형 문장으로 작성> - -## Acceptance Criteria -1. Given <선행 조건>, When <행동>, Then <기대 결과> -2. Given <선행 조건>, When <행동>, Then <기대 결과> - -## Impacted Area -- Docs: <영향 받는 문서 경로> -- Modules: <영향 받는 모듈/컴포넌트> -- Tests: <추가/수정해야 하는 테스트> - -## Notes -- <가정, 제약, 미해결 이슈> - -## Change Log - -### CHG-YYYYMMDD-01 -- Date: YYYY-MM-DD -- Author: <작성자> - -#### Change Summary -- <무엇이 변경되었는지> - -#### Intent -- <왜 변경했는지> - -#### Acceptance Criteria Delta -- Added: - - <추가된 기준> -- Updated: - - <변경된 기준> -- Removed: - - <삭제된 기준> - -#### Impacted Area Delta -- Docs: - - <문서 영향> -- Modules: - - <코드 영향> -- Tests: - - <테스트 영향> - -#### Validation Plan -- <검증 방법과 완료 조건> diff --git a/.vendor/myagent-kit/templates/docs/SRS/TRACEABILITY-template.md b/.vendor/myagent-kit/templates/docs/SRS/TRACEABILITY-template.md deleted file mode 100644 index 9076648..0000000 --- a/.vendor/myagent-kit/templates/docs/SRS/TRACEABILITY-template.md +++ /dev/null @@ -1,28 +0,0 @@ -# SRS Traceability Matrix - - - -## Status Definition -- Planned: Requirement is defined but not yet implemented. -- In Progress: Implementation issue/PR exists and work is ongoing. -- Implemented: Code is merged and mapped acceptance criteria are complete. -- Verified: Required tests and manual checks are completed. -- Released: Included in a released version. - -## Matrix - -| REQ ID | AC Mapping | Requirement File | Implementation Issue | PR | Test Evidence | Requirement Status | Delivery Status | Last Updated | -|---|---|---|---|---|---|---|---|---| -| REQ-001 | AC1, AC2 | docs/SRS//REQ-001_.md | #123 | #456 | tests/... , CI run link | Approved | Planned | YYYY-MM-DD | - -## Update Rules -- REQ ID must exactly match the SRS requirement file prefix. -- AC Mapping should reference concrete IDs such as AC1, AC2. -- Test Evidence should point to executable artifacts: test file path, CI job, or manual verification note. -- Last Updated uses YYYY-MM-DD. -- If there is no value yet, use `TBD`. diff --git a/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md b/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md deleted file mode 100644 index 42e8832..0000000 --- a/base/.github/ISSUE_TEMPLATE/implementation-from-approved-req.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: "Implementation From Approved Requirement" -about: "Create an implementation issue from an Approved SRS requirement" -title: "[REQ-XXX] Implement " -labels: ["implementation", "requirement"] -assignees: [] ---- - - - -## 배경 (Context) -- Requirement ID: -- Requirement Link: -- Status Check: Approved - -## 범위 (Scope) -- In Scope: -- Out of Scope: - -## 구현 전 설계검토 체크리스트 (Pre-Implementation Design Review) -- [ ] Architecture approach is documented and agreed -- [ ] Public interface/API contract changes are identified -- [ ] Data model/schema impact is reviewed -- [ ] Error handling and fallback strategy is defined -- [ ] Security/privacy impact is reviewed -- [ ] Performance/scalability impact is reviewed -- [ ] Test strategy (unit/integration/e2e/manual) is defined -- [ ] Rollback or feature-flag strategy is defined when needed -- [ ] Unknowns, assumptions, and decision owners are recorded -- [ ] Human review sign-off completed (or `No additional design review required.`) - -### 설계 결정 요약 (Design Decisions) -- Decision 1: -- Decision 2: - -### 미해결 항목 / 가정 (Open Items / Assumptions) -- - -## AI Agent 작업 (AI Agent Work) -- [ ] Code change task 1 -- [ ] Code change task 2 -- [ ] Automated validation task - -## 사람 작업 (Human Work) -- [ ] Approve scope or open questions -- [ ] Perform required manual verification - -## Acceptance Criteria Mapping -- REQ AC1 -> Task(s): -- REQ AC2 -> Task(s): - -## AI 검증 요약 (AI Validation Summary) -- Automated tests/checks to run: -- Expected evidence: - -## 사람 확인 체크리스트 (Human Verification Checklist) -- [ ] Verify: -- [ ] Confirm: -- If no human verification is required, write: `No manual verification required.` - -## 열린 질문 (Open Questions) -- - -## 리스크 (Risks) -- - -## 완료 조건 (Definition of Done) -- [ ] All mapped acceptance criteria are satisfied -- [ ] Tests are added or updated -- [ ] AI-completable work is finished and evidence is attached -- [ ] Human-only work is completed or explicitly marked not required -- [ ] Docs or change notes are updated if needed diff --git a/base/.github/instructions/requirement-scope-decision.instructions.md b/base/.github/instructions/requirement-scope-decision.instructions.md deleted file mode 100644 index b3df18a..0000000 --- a/base/.github/instructions/requirement-scope-decision.instructions.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -description: "Use when evaluating whether a new feature or change request requires a new or updated SRS requirement, a technical spec update only, or needs owner decision. Apply before starting requirement drafting." -applyTo: "docs/SRS/**/*.md" ---- -# Requirement Scope Decision Guidelines - -## 목적 -새 기능 요청이나 변경 사항이 들어왔을 때, SRS에 등록해야 하는지(신규 또는 기존 REQ 수정) 아니면 technical spec / implementation note만 업데이트하면 되는지를 먼저 판정한다. -이 판정을 거침으로써 불필요한 REQ 생성을 방지하고, SRS에 등록돼야 할 변경이 누락되는 것을 예방한다. - -## 판정 결과 분류 (4종) -1. **New SRS requirement needed** — 새 REQ 문서를 작성해야 한다. -2. **Update existing SRS requirement** — 기존 REQ 문서를 수정해야 한다. -3. **Technical spec only** — SRS 변경 없이 technical spec / implementation note만 업데이트한다. -4. **Needs owner decision** — 운영 리스크나 승인 기준이 불명확하여 소유자(Owner) 결정이 필요하다. - -## SRS 대상 판정 기준 (다음 중 하나라도 해당하면 SRS) - -### 외부 계약 변화 -- 사용자 또는 외부 시스템이 관찰하는 계약이 바뀌는 경우 -- API / CLI / UI / Event / Data schema / Config / Deployment / Integration 요구사항이 변경되는 경우 - -### 측정 가능한 Non-Functional 변경 -- 성능, 보안, 가용성, 확장성 등 측정 기준이 추가되거나 변경되는 경우 - -### Acceptance Criteria 및 승인 게이트 필요 -- 변경 사항에 대해 명시적 수락 기준과 승인 이력이 필요한 경우 - -## Technical Spec Only 판정 기준 (다음에 해당하면 SRS 불필요) -- 내부 리팩터링 (외부 계약 변화 없음) -- 라이브러리 교체 (동일한 외부 인터페이스 유지) -- 코드 구조 / 패턴 변경 (기능 동작 동일) -- 외부 계약을 바꾸지 않는 구현 최적화 - -## Borderline / Needs Owner Decision 판정 기준 -- 운영 리스크나 승인 책임이 불명확한 경우 -- 제품 의도가 불분명하거나 범위가 애매한 경우 -- 위 기준만으로 분류가 확정되지 않는 경우 - -## New vs. Update 구분 기준 -- 동일 기능/계약 범위의 변경 → `Update existing SRS requirement` -- 새 기능/계약/인터페이스의 추가 → `New SRS requirement needed` -- 기존 REQ 검색 시 `docs/SRS/index.md`의 Requirement Register를 먼저 확인한다. - -## 판정 출력 형식 -판정 결과는 항상 다음 항목을 포함해야 한다. -1. **Decision** — 위 4종 중 하나 -2. **Why** — 판정 근거 (구체적으로 어떤 기준에 해당하는지) -3. **Recommended Type** — SRS 대상인 경우, 해당 requirement type (Interface / Data / Deployment 등) -4. **Existing REQ candidates** — Update 가능성이 있는 기존 REQ ID 목록 (해당 시) -5. **Next Action** — 판정 결과에 따른 다음 단계 안내 -6. **Open Questions** — 추가 확인이 필요한 사항 (없으면 `없음`) diff --git a/base/.github/instructions/srs.instructions.md b/base/.github/instructions/srs.instructions.md deleted file mode 100644 index d2e3533..0000000 --- a/base/.github/instructions/srs.instructions.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -description: "Use when writing or editing SRS documents, requirement files, acceptance criteria, change logs, or impacted area entries under docs/SRS. Ensures requirement quality, traceability, and testable wording." -applyTo: "docs/SRS/**/*.md" ---- -# SRS Document Guidelines - -## Requirement 타입 분류 - -### 분류 의사결정 규칙 (우선순위 순) -1. **Installation**: "시스템 설치, 환경 구성, 초기화 절차를 정의하는가?" → YES면 Installation - - 예: Docker 이미지 빌드, 데이터베이스 마이그레이션, 사전 조건 설정 -2. **Interface**: "사용자가 직접 조작하거나 실행하는 방식을 정의하는가?" → YES면 Interface - - 예: CLI 명령어, API 엔드포인트, 웹 UI 기능, 제어 방식 -3. **Configuration**: "시스템 또는 기능의 설정 옵션, 커스터마이징을 정의하는가?" → YES면 Configuration - - 예: 설정 파라미터, 옵션 항목, 환경 변수, 커스터마이징 포인트 -4. **Data**: "데이터 저장, 로드, 구조, 파일 형식, 스키마를 정의하는가?" → YES면 Data - - 예: 설정 파일 형식, 데이터베이스 스키마, API 요청/응답 구조 -5. **Exception & Error Handling**: "오류 처리, 예외 복구, 재시도 전략을 중심으로 하는가?" → YES면 Exception & Error Handling - - 예: 네트워크 오류 재시도, 데이터 검증 실패 처리, 복구 절차 -6. **Deployment**: "배포 방식, 운영 절차, 배포 환경을 정의하는가?" → YES면 Deployment - - 예: 컨테이너 배포, 스택 관리, 운영 가이드 -7. **Documentation**: "문서 작성, 가이드, 매뉴얼, 안내서를 정의하는가?" → YES면 Documentation - - 예: 사용자 가이드, API 문서, 운영 매뉴얼, 튜토리얼 -8. **Integration**: "외부 시스템 연동, 플러그인, 확장 기능을 정의하는가?" → YES면 Integration - - 예: 3rd party 서비스 연동, 플러그인 인터페이스, 확장 메커니즘 -9. **Migration**: "데이터 이전, 버전 업그레이드, 마이그레이션을 정의하는가?" → YES면 Migration - - 예: 데이터 마이그레이션, 버전 호환성, 업그레이드 절차 -10. **Testing**: "테스트 전략, QA 요구사항, 검증 방식을 정의하는가?" → YES면 Testing - - 예: 테스트 커버리지, 회귀 테스트, 수락 테스트 기준 -11. **Non-Functional**: 위 모든 경우에 해당하지 않으면 → Non-Functional - - 예: 성능 목표, 보안 정책, 확장성, 호환성, 신뢰성 요구사항 - -**주의**: requirement가 여러 분류에 해당하면 주된 제약 또는 목적에 따라 하나로만 선택한다. 예를 들어: -- "Docker Compose로 배포할 수 있어야 한다" → Deployment -- "배포 시 환경 변수로 설정 가능해야 한다" → Configuration -- "배포 전에 다음 점검 항목을 검증해야 한다" → Testing/Documentation - -- `docs/SRS/index.md`는 SRS 운영 기준과 Requirement Register를 관리하는 문서로 유지한다. -- `docs/SRS/REQ-XXX_*.md`는 하나의 requirement만 다루고, 다른 requirement 내용을 섞지 않는다. 파일은 Type별 폴더에 저장한다. 예: `docs/SRS/Deployment/REQ-001_docker-compose-release.md` -- 새 requirement 번호의 source of truth는 `docs/SRS/index.md`의 `Next Requirement ID`다. -- requirement 본문에는 최소한 `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log`가 포함되어야 한다. -- `Acceptance Criteria`는 관찰 가능하고 test 가능한 문장으로 작성한다. -- 모호한 표현("개선한다", "적절히 처리한다", "최적화한다")은 측정 기준 없이 사용하지 않는다. -- requirement를 수정하면 해당 REQ 파일의 `Change Log`도 함께 갱신한다. -- `Impacted Area`는 `Docs`, `Modules`, `Tests`를 모두 검토한 뒤 작성한다. 해당 항목이 없으면 `없음`으로 명시한다. -- implementation issue/PR를 생성하거나 병합할 때는 `docs/SRS/traceability-matrix.md`에서 해당 REQ 행의 `Issue/PR`, `Tests`, `Status`를 갱신한다. -- 새로운 Type으로 requirement를 추가할 때는 `docs/SRS//` 폴더 하위에 저장한다. 아직 없는 Type 폴더면 생성한다. -- 새로운 requirement를 추가하면 `docs/SRS/index.md`의 Requirement Register에도 같은 ID를 추가한다. -- 새로운 requirement를 추가하거나 Register가 바뀌면 `docs/SRS/index.md`의 `Next Requirement ID`도 함께 갱신한다. -- `docs/SRS/index.md`의 `Recent Change Log Summary`는 최신 5건만 유지하고 최신 항목을 위에 둔다. -- requirement ID와 파일명 prefix는 일치해야 한다. 예: `REQ-002`는 `docs/SRS//REQ-002_.md`. -- requirement 상태를 `Approved` 또는 `Implemented`로 바꿀 때는 acceptance criteria와 change history가 누락되지 않았는지 먼저 검토한다. -- requirement 상태를 전환할 때는 `docs/SRS/index.md`의 `Status Transition Evidence` 기준을 따른다. diff --git a/base/.github/prompts/approve-requirement.prompt.md b/base/.github/prompts/approve-requirement.prompt.md deleted file mode 100644 index 3bf3267..0000000 --- a/base/.github/prompts/approve-requirement.prompt.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -description: "Use when: decide whether a Proposed requirement is ready to move to Approved and generate the required status-change edits" -name: "Approve Requirement" -argument-hint: "Paste a Proposed REQ file or select requirement text" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서가 `Proposed`에서 `Approved`로 전환 가능한지 검토한다. - -입력: -${input} - -검토 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 의 `Status Transition Evidence`와 해당 requirement 문서 구조다. -- `Status`, `Type`, `Priority`, `Owner`, `Intent`, `Requirement`, `Acceptance Criteria`, `Impacted Area`, `Change Log` 존재 여부를 검토한다. -- Type이 SRS index의 분류 도표에 정의된 타입 중 정확하게 하나로 지정되었는지 확인한다. -- `Acceptance Criteria`가 구현 기준으로 충분히 구체적인지 판단한다. -- 중요한 assumption 또는 open question이 남아 있으면 승인 blocker로 본다. -- 승인 가능하면 status 변경과 Change Log 추가 초안을 함께 제안한다. -- 승인 불가하면 blocker를 우선순위 순으로 정리한다. - -출력 형식: -1. Approval Decision: `Approved-ready` 또는 `Not ready` -2. Blockers -3. Suggested Edits -4. Approval Change Log Draft diff --git a/base/.github/prompts/complete-requirement.prompt.md b/base/.github/prompts/complete-requirement.prompt.md deleted file mode 100644 index ea0de9a..0000000 --- a/base/.github/prompts/complete-requirement.prompt.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -description: "Use when: complete a partial SRS requirement draft into a full REQ file with acceptance criteria and impacted area" -name: "Complete Requirement Draft" -argument-hint: "Paste a partial requirement draft or describe the feature" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기반으로 SRS requirement 초안을 완성한다. - -입력: -${input} - -작업 규칙: -- SRS 기준 문서는 [SRS index](../../docs/SRS/index.md) 와 관련 [REQ template](../../docs/SRS/REQ-template.md) 를 따른다. -- 입력에 `REQ-XXX` 형식의 명시적 ID가 없으면, [SRS index](../../docs/SRS/index.md)의 `Next Requirement ID`를 우선 읽어 그 값을 사용한다. `Next Requirement ID`가 없거나 신뢰할 수 없으면 `Requirement Register`와 기존 REQ 파일들 기준의 최고 번호에 1을 더한 값을 사용한다. -- 사용자가 이미 특정 REQ ID를 명시했으면 그 ID를 유지하고 임의로 바꾸지 않는다. -- requirement의 유형을 인식하고 `Type` 필드를 정확하게 지정한다. 분류는 [SRS index](../../docs/SRS/index.md)의 "Requirement 작성 규칙" 또는 [SRS instructions](../../.github/instructions/srs.instructions.md)의 "분류 의사결정 규칙"을 따른다. -- 최종 `Type`과 `REQ ID`를 이용해 추천 파일 경로를 함께 제시한다. 형식은 `docs/SRS//REQ-XXX_.md`를 사용한다. -- ``는 requirement 제목을 기반으로 소문자 kebab-case 영문으로 생성한다. 공백은 `-`로 치환하고, 영문/숫자/`-`만 남긴다. -- 제목이 한글이거나 slug 생성이 불명확하면 의미를 유지하는 짧은 영문 slug를 보수적으로 제안한다. -- 사용자가 제공한 의도와 핵심 동작을 보존한다. -- 빠진 항목은 보수적으로 보완하되, 추정이 필요한 내용은 `Notes`에 assumption으로 분리한다. -- `Acceptance Criteria`는 반드시 test 가능하게 작성한다. -- `Impacted Area`는 Docs, Modules, Tests로 구분한다. -- 새로 할당한 ID를 문서 제목 `# REQ-XXX: ...`에 반영한다. -- 결과는 REQ 파일에 바로 붙여넣을 수 있는 Markdown 형식으로만 출력한다. -- 코드 구현 계획은 포함하지 않는다. - -출력 형식: -1. `Recommended File Path: docs/SRS//REQ-XXX_.md` 한 줄 -2. 실제 할당된 ID가 반영된 `# REQ-XXX: ...` 형식의 전체 문서 본문 -3. `## Change Log` 아래에 초안 생성용 `CHG-YYYYMMDD-01` 항목 1개 포함 diff --git a/base/.github/prompts/create-implementation-issue.prompt.md b/base/.github/prompts/create-implementation-issue.prompt.md deleted file mode 100644 index 1566340..0000000 --- a/base/.github/prompts/create-implementation-issue.prompt.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -description: "Use when: create an implementation plan and GitHub issue draft from an Approved requirement" -name: "Create Implementation Issue" -argument-hint: "Paste an Approved REQ file" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 `Approved` requirement 문서를 기반으로 implementation issue 초안을 생성한다. - -입력: -${input} - -작업 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 와 해당 REQ 문서다. -- 출력 언어는 기본적으로 한국어로 작성한다. -- section 제목, 설명, 작업 지시문은 한국어를 우선 사용하되, 중요한 technical term는 영어를 유지하거나 한국어 뒤 괄호로 병기한다. 예: `AI Agent Work`, `Human Verification Checklist`, `Acceptance Criteria`, `Open Questions`, `Risk`, `Definition of Done`. -- 코드 식별자, 파일 경로, 명령어, API 이름, 테스트 이름, UI 라벨 등 구현 대상과 직접 연결되는 literal은 영어 원문을 유지한다. -- requirement의 `Acceptance Criteria`를 구현 task로 분해한다. -- task는 구현 단위로 나누고, 각 task를 `AI Agent가 완료 가능한 작업`과 `Human이 수행해야 하는 작업`으로 구분한다. -- test task를 반드시 포함한다. -- 각 acceptance criteria 또는 task에 대해 다음을 판단한다: - - AI Agent가 코드 변경과 자동 검증까지 완료할 수 있는가 - - Human이 직접 확인해야 하는 수동 검증이 남는가 -- Human이 직접 확인해야 하는 항목이 있으면 `Human Verification Checklist`에 구체적인 확인 절차와 기대 결과를 작성한다. -- AI Agent가 자체적으로 테스트를 완료할 수 있어 Human 확인이 불필요하면 `Human Verification Checklist`에 `No manual verification required.`를 명시하고 그 근거를 `AI Validation Summary`에 적는다. -- `Impacted Area`를 기반으로 module/docs/test 변경 범위를 제시한다. -- 미해결 가정이나 리스크가 있으면 issue 본문에 `Open Questions`와 `Risks`로 명시한다. -- 출력은 GitHub issue에 바로 붙여 넣을 수 있는 Markdown 형식으로만 작성한다. - -출력 형식: -1. Issue Title -2. 배경 (Context) -3. 범위 (Scope) -4. AI Agent 작업 (AI Agent Work) 체크리스트 -5. 사람 작업 (Human Work) 체크리스트 -6. Acceptance Criteria Mapping (REQ 기준) -7. AI 검증 요약 (AI Validation Summary) -8. 사람 확인 체크리스트 (Human Verification Checklist) -9. 열린 질문 (Open Questions) -10. 리스크 (Risks) -11. 완료 조건 (Definition of Done) -12. GitHub Issue Create Command (`gh issue create`) 예시 diff --git a/base/.github/prompts/decide-requirement-scope.prompt.md b/base/.github/prompts/decide-requirement-scope.prompt.md deleted file mode 100644 index 4a847ec..0000000 --- a/base/.github/prompts/decide-requirement-scope.prompt.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -description: "Use when: evaluate whether a new feature or change request needs a new SRS requirement, an update to an existing one, a technical spec update only, or owner decision — before starting requirement drafting" -name: "Decide Requirement Scope" -argument-hint: "Describe the feature or change request: include feature description, issue body, and/or PR/diff summary" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기반으로, 이 변경이 SRS 대상인지 아닌지를 판정한다. - -입력: -${input} - -판정 규칙: -- 기준 문서는 [requirement-scope-decision instructions](../../.github/instructions/requirement-scope-decision.instructions.md) 와 [SRS index](../../docs/SRS/index.md) 의 Requirement Register다. -- 다음 4가지 결과 중 하나를 반드시 반환한다. - 1. `New SRS requirement needed` - 2. `Update existing SRS requirement` - 3. `Technical spec only` - 4. `Needs owner decision` -- 판정 기준을 순서대로 적용한다. - - 사용자/외부 시스템이 관찰하는 계약(API, CLI, UI, Event, Data schema, Config, Deployment, Integration)이 바뀌는가? → YES면 SRS - - 측정 가능한 Non-Functional requirement(성능, 보안, 가용성 등)가 추가/변경되는가? → YES면 SRS - - Acceptance Criteria 및 명시적 승인 게이트가 필요한가? → YES면 SRS - - 위 기준에 모두 해당하지 않으면서 내부 리팩터링·라이브러리 교체·코드 구조 변경·구현 최적화인가? → Technical spec only - - 위 기준만으로 분류가 확정되지 않거나 운영 책임/리스크가 불명확한가? → Needs owner decision -- SRS 대상으로 판정되면, 신규 기능/계약인지 기존 REQ 범위 내 변경인지를 구분한다. - - 기존 REQ에 해당하는 경우 [SRS index](../../docs/SRS/index.md) 의 Requirement Register에서 후보 REQ ID를 찾아 제시한다. -- Technical spec only로 판정되면, 불필요한 REQ 생성을 명시적으로 피하도록 안내한다. -- 결과에는 항상 `Why`와 `Next Action`을 포함한다. -- `Open Questions`가 없으면 `없음`으로 명시한다. - -출력 형식: -1. **Decision**: `New SRS requirement needed` / `Update existing SRS requirement` / `Technical spec only` / `Needs owner decision` 중 하나 -2. **Why**: 판정 근거 — 어떤 기준에 해당하는지 구체적으로 설명 -3. **Recommended Type**: (SRS 대상인 경우만) Interface / Data / Deployment / Configuration 등 해당 requirement type -4. **Existing REQ candidates**: (Update 가능성이 있는 경우만) 관련 REQ ID와 제목 목록. 해당 없으면 생략 -5. **Next Action**: 판정 결과에 따른 다음 단계 안내 -6. **Open Questions**: 추가 확인이 필요한 사항. 없으면 `없음` diff --git a/base/.github/prompts/mark-requirement-implemented.prompt.md b/base/.github/prompts/mark-requirement-implemented.prompt.md deleted file mode 100644 index b1567f5..0000000 --- a/base/.github/prompts/mark-requirement-implemented.prompt.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -description: "Use when: decide whether an Approved requirement is ready to move to Implemented and generate the required status-change edits" -name: "Mark Requirement Implemented" -argument-hint: "Paste an Approved REQ file and implementation evidence" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서와 implementation evidence를 검토해 `Approved`에서 `Implemented`로 전환 가능한지 판단한다. - -입력: -${input} - -검토 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 의 `Status Transition Evidence`와 해당 requirement 문서 구조다. -- requirement의 `Acceptance Criteria`가 implementation evidence, tests, 또는 manual verification으로 충족되었는지 확인한다. -- 충족되지 않은 acceptance criteria가 있으면 구현 완료로 판정하지 않는다. -- 구현 완료 가능하면 status 변경, 잔여 리스크, Change Log 추가 초안을 함께 제안한다. -- 구현 완료 불가하면 미충족 기준과 필요한 검증 항목을 정리한다. - -출력 형식: -1. Implementation Decision: `Implemented-ready` 또는 `Not ready` -2. Unverified Criteria -3. Suggested Edits -4. Implementation Change Log Draft -5. Residual Risk diff --git a/base/.github/prompts/sync-srs-index.prompt.md b/base/.github/prompts/sync-srs-index.prompt.md deleted file mode 100644 index e8ecc03..0000000 --- a/base/.github/prompts/sync-srs-index.prompt.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -description: "Use when: update docs/SRS/index.md Requirement Register or Recent Change Log Summary after adding or editing REQ files" -name: "Sync SRS Index" -argument-hint: "Paste changed REQ files or describe what changed in SRS" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 입력을 기준으로 `docs/SRS/index.md`를 갱신하기 위한 제안을 만든다. - -입력: -${input} - -작업 규칙: -- 기준 문서는 [SRS index](../../docs/SRS/index.md) 이다. -- 새 requirement가 추가되었거나 Register가 바뀌면 `Requirement Register`의 최고 번호를 기준으로 다음 번호를 계산해 `Next Requirement ID`도 함께 갱신한다. -- 새 requirement가 추가되었으면 `Requirement Register`에 들어갈 행을 만든다. -- 기존 requirement가 수정되었으면 `Recent Change Log Summary`에 들어갈 요약 항목을 만든다. -- 기존 구조를 바꾸지 말고, 필요한 행과 요약만 제안한다. -- `Next Requirement ID`는 `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 큰 값이어야 한다. -- `Recent Change Log Summary`는 최신 변경을 위에 두고, 짧은 요약 1줄로 작성한다. -- `Recent Change Log Summary`는 최신 5건만 유지하도록 제안한다. 5건을 초과하면 제거 또는 대체해야 할 가장 오래된 항목도 함께 설명한다. -- 출력은 사람이 `docs/SRS/index.md`에 바로 반영할 수 있는 Markdown 조각으로만 작성한다. - -출력 형식: -1. `Next Requirement ID`에 반영할 Markdown bullet -2. `Requirement Register`에 추가 또는 수정할 Markdown table row -3. `Recent Change Log Summary`에 추가할 Markdown section -4. 필요한 경우 only, index에서 제거 또는 갱신해야 할 기존 항목 설명 diff --git a/base/.github/prompts/validate-requirement.prompt.md b/base/.github/prompts/validate-requirement.prompt.md deleted file mode 100644 index bbb9ba1..0000000 --- a/base/.github/prompts/validate-requirement.prompt.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -description: "Use when: review an SRS requirement for ambiguity, missing acceptance criteria, missing impacted area, and change-log gaps" -name: "Validate Requirement Quality" -argument-hint: "Paste a REQ file or select requirement text" -agent: "agent" -model: "GPT-5 (copilot)" ---- -다음 requirement 문서를 검토한다. - -입력: -${input} - -검토 규칙: -- 기준은 [SRS index](../../docs/SRS/index.md) 와 관련 requirement 문서 구조를 따른다. -- 다음 항목을 우선 검토한다: 모호한 표현, test 불가능한 문장, 누락된 acceptance criteria, 누락된 impacted area, 상태/유형/우선순위/owner 누락, Type이 의사결정 규칙에 따라 정확하게 지정되었는지, Change Log 누락 또는 불일치. - - Type 분류 규칙: 사용자 조작 방식 → Interface, 데이터 저장/구조 → Data, 오류 처리 → Exception & Error Handling, 그 외 → Non-Functional -- 중요한 문제부터 우선순위 순으로 정리한다. -- 문제가 없으면 `No critical findings.` 로 시작한다. -- requirement 본문을 직접 다시 작성하지 말고, 수정 제안 위주로 답한다. - -출력 형식: -1. Findings -2. Open Questions -3. Suggested Edits diff --git a/base/.github/pull_request_template.md b/base/.github/pull_request_template.md deleted file mode 100644 index 4af7096..0000000 --- a/base/.github/pull_request_template.md +++ /dev/null @@ -1,24 +0,0 @@ -## Requirement Traceability - -- Requirement ID: REQ-XXX -- Requirement Link: docs/SRS//REQ-XXX_.md -- Related Issue: # - -## Acceptance Criteria Coverage -- [ ] AC1: -- [ ] AC2: - -## Test Evidence -- Automated: - - -- Manual: - - - - If not required: `No manual verification required.` - -## Impacted Area Check -- Docs: -- Modules: -- Tests: - -## Release Notes Input -- [ ] Added REQ ID to release notes (or tracked release note draft) diff --git a/bootstrap/install.sh b/bootstrap/install.sh deleted file mode 100755 index 31dafc2..0000000 --- a/bootstrap/install.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -KIT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -PROJECT_ROOT="${1:-$(pwd)}" - -mkdir -p "$PROJECT_ROOT/.github/prompts" \ - "$PROJECT_ROOT/.github/instructions" \ - "$PROJECT_ROOT/.github/ISSUE_TEMPLATE" - -cp "$KIT_ROOT/base/.github/prompts/"*.prompt.md "$PROJECT_ROOT/.github/prompts/" -cp "$KIT_ROOT/base/.github/instructions/"*.instructions.md "$PROJECT_ROOT/.github/instructions/" -cp "$KIT_ROOT/base/.github/ISSUE_TEMPLATE/"*.md "$PROJECT_ROOT/.github/ISSUE_TEMPLATE/" -if [[ -f "$KIT_ROOT/base/.github/pull_request_template.md" ]]; then - cp "$KIT_ROOT/base/.github/pull_request_template.md" "$PROJECT_ROOT/.github/pull_request_template.md" -fi - -echo "Installed myagent-kit assets into $PROJECT_ROOT/.github" From ec20f933ad570ddf3917e7580aefa746e53e9ebf Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 22:09:53 +0900 Subject: [PATCH 11/21] update stage-pilot --- .github/AGENTS.md | 23 ++++ .github/START-HERE.md | 61 +++++++++ .github/agents/builder.agent.md | 46 +++++++ .github/agents/planner.agent.md | 46 +++++++ .github/agents/releaser.agent.md | 46 +++++++ .github/agents/reviewer.agent.md | 46 +++++++ .github/copilot-instructions.md | 43 +++++++ .../instructions/01-discovery.instructions.md | 108 ++++++++++++++++ .../instructions/02-planning.instructions.md | 114 +++++++++++++++++ .../instructions/03-design.instructions.md | 49 ++++++++ .../04-implementation.instructions.md | 49 ++++++++ .../05-verification.instructions.md | 117 ++++++++++++++++++ .../instructions/06-release.instructions.md | 72 +++++++++++ .../07-operations.instructions.md | 78 ++++++++++++ .github/prompts/design-confirm.prompt.md | 63 ++++++++++ .github/prompts/design-draft.prompt.md | 85 +++++++++++++ .github/prompts/design-review.prompt.md | 80 ++++++++++++ .github/prompts/discovery-confirm.prompt.md | 55 ++++++++ .github/prompts/discovery-draft.prompt.md | 107 ++++++++++++++++ .github/prompts/discovery-review.prompt.md | 80 ++++++++++++ .../prompts/implementation-confirm.prompt.md | 64 ++++++++++ .../prompts/implementation-draft.prompt.md | 85 +++++++++++++ .github/prompts/implementation.prompt.md | 75 +++++++++++ .github/prompts/operation-confirm.prompt.md | 67 ++++++++++ .github/prompts/operation-draft.prompt.md | 85 +++++++++++++ .github/prompts/operation-review.prompt.md | 63 ++++++++++ .github/prompts/planning-confirm.prompt.md | 56 +++++++++ .github/prompts/planning-draft.prompt.md | 95 ++++++++++++++ .github/prompts/planning-review.prompt.md | 79 ++++++++++++ .github/prompts/release-confirm.prompt.md | 63 ++++++++++ .github/prompts/release-draft.prompt.md | 85 +++++++++++++ .github/prompts/release-review.prompt.md | 80 ++++++++++++ .../prompts/verification-confirm.prompt.md | 63 ++++++++++ .github/prompts/verification-draft.prompt.md | 84 +++++++++++++ .github/prompts/verification-review.prompt.md | 83 +++++++++++++ .github/scripts/bootstrap.sh | 102 +++++++++++++++ .github/skills/release-notes/SKILL.md | 26 ++++ .../skills/requirements-refinement/SKILL.md | 26 ++++ .github/skills/test-design/SKILL.md | 26 ++++ .github/templates/design-note.template.md | 71 +++++++++++ .github/templates/discovery-note.template.md | 90 ++++++++++++++ .../templates/implementation-note.template.md | 62 ++++++++++ .github/templates/operations-note.template.md | 80 ++++++++++++ .../planning-backlog-scores.template.csv | 4 + .github/templates/planning-note.template.md | 63 ++++++++++ .../planning-scoring-sheet.template.csv | 4 + .github/templates/release-note.template.md | 105 ++++++++++++++++ .../templates/rollback-runbook.template.md | 62 ++++++++++ .../templates/verification-note.template.md | 83 +++++++++++++ .github/tools/calc_priority_scores.sh | 56 +++++++++ 50 files changed, 3355 insertions(+) create mode 100644 .github/AGENTS.md create mode 100644 .github/START-HERE.md create mode 100644 .github/agents/builder.agent.md create mode 100644 .github/agents/planner.agent.md create mode 100644 .github/agents/releaser.agent.md create mode 100644 .github/agents/reviewer.agent.md create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/01-discovery.instructions.md create mode 100644 .github/instructions/02-planning.instructions.md create mode 100644 .github/instructions/03-design.instructions.md create mode 100644 .github/instructions/04-implementation.instructions.md create mode 100644 .github/instructions/05-verification.instructions.md create mode 100644 .github/instructions/06-release.instructions.md create mode 100644 .github/instructions/07-operations.instructions.md create mode 100644 .github/prompts/design-confirm.prompt.md create mode 100644 .github/prompts/design-draft.prompt.md create mode 100644 .github/prompts/design-review.prompt.md create mode 100644 .github/prompts/discovery-confirm.prompt.md create mode 100644 .github/prompts/discovery-draft.prompt.md create mode 100644 .github/prompts/discovery-review.prompt.md create mode 100644 .github/prompts/implementation-confirm.prompt.md create mode 100644 .github/prompts/implementation-draft.prompt.md create mode 100644 .github/prompts/implementation.prompt.md create mode 100644 .github/prompts/operation-confirm.prompt.md create mode 100644 .github/prompts/operation-draft.prompt.md create mode 100644 .github/prompts/operation-review.prompt.md create mode 100644 .github/prompts/planning-confirm.prompt.md create mode 100644 .github/prompts/planning-draft.prompt.md create mode 100644 .github/prompts/planning-review.prompt.md create mode 100644 .github/prompts/release-confirm.prompt.md create mode 100644 .github/prompts/release-draft.prompt.md create mode 100644 .github/prompts/release-review.prompt.md create mode 100644 .github/prompts/verification-confirm.prompt.md create mode 100644 .github/prompts/verification-draft.prompt.md create mode 100644 .github/prompts/verification-review.prompt.md create mode 100644 .github/scripts/bootstrap.sh create mode 100644 .github/skills/release-notes/SKILL.md create mode 100644 .github/skills/requirements-refinement/SKILL.md create mode 100644 .github/skills/test-design/SKILL.md create mode 100644 .github/templates/design-note.template.md create mode 100644 .github/templates/discovery-note.template.md create mode 100644 .github/templates/implementation-note.template.md create mode 100644 .github/templates/operations-note.template.md create mode 100644 .github/templates/planning-backlog-scores.template.csv create mode 100644 .github/templates/planning-note.template.md create mode 100644 .github/templates/planning-scoring-sheet.template.csv create mode 100644 .github/templates/release-note.template.md create mode 100644 .github/templates/rollback-runbook.template.md create mode 100644 .github/templates/verification-note.template.md create mode 100755 .github/tools/calc_priority_scores.sh diff --git a/.github/AGENTS.md b/.github/AGENTS.md new file mode 100644 index 0000000..5cdd60d --- /dev/null +++ b/.github/AGENTS.md @@ -0,0 +1,23 @@ +# SDLC Role Agents + +이 프로젝트는 단계 책임을 명확히 하기 위해 역할별 에이전트를 사용한다. + +## Roles + +1. Planner: Discovery, Planning, Design 중심 +2. Builder: Implementation 중심 +3. Reviewer: Verification 중심 +4. Releaser: Release, Operations 중심 + +## Handoff Rules + +1. Planner -> Builder: 우선순위, 수용 기준, 설계 근거를 전달한다. +2. Builder -> Reviewer: 변경 요약, 테스트 결과, 알려진 리스크를 전달한다. +3. Reviewer -> Releaser: 품질 상태와 배포 가능 여부를 전달한다. +4. Releaser -> Planner: 운영 피드백과 개선 항목을 다음 사이클로 전달한다. + +## Gate Rules + +1. 각 역할은 자신의 단계 게이트를 통과시키기 전 다음 역할로 넘길 수 없다. +2. Blocker 이슈는 우회가 아닌 해결 또는 승인된 예외로만 처리한다. +3. 긴급 핫픽스 후에는 반드시 Planner 단계로 환류해 누락 문서를 보강한다. diff --git a/.github/START-HERE.md b/.github/START-HERE.md new file mode 100644 index 0000000..118f035 --- /dev/null +++ b/.github/START-HERE.md @@ -0,0 +1,61 @@ +# SDLC Project Start Guide + +## 1. Kickoff Order + +1. Discovery 문서 작성 +2. Planning 점수표 산정(WSJF/RICE) +3. Design 근거 기록 +4. Implementation + 테스트 +5. Verification 게이트 통과 +6. Release 체크리스트 + 런북 준비 +7. Operations 모니터링 및 환류 + +### Discovery 저장 규칙 + +1. 저장 폴더: docs/sdlc/discovery/ +2. 파일명 형식: dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md +3. 날짜는 생성일 기준으로 유지 +4. 작성 템플릿: .github/templates/discovery-note.template.md +5. 문서 하단 Discovery Freeze 필수 + +## 2. Planning Scoring Quick Start + +1. 템플릿 복사: .github/templates/planning-backlog-scores.template.csv +2. 항목 입력 후 자동 계산 실행: + +```bash +bash .github/tools/calc_priority_scores.sh .github/templates/planning-backlog-scores.template.csv docs/sdlc/planning/planning-backlog-scores.out.csv +``` + +3. 산출물 확인: selected_score 내림차순으로 priority 부여됨 + +## 3. Rollback Runbook Quick Start + +1. 기본 템플릿: .github/templates/rollback-runbook.template.md +2. 서비스별 인스턴스: + - .github/runbooks/services/api.rollback-runbook.md + - .github/runbooks/services/worker.rollback-runbook.md + - .github/runbooks/services/web.rollback-runbook.md +3. 배포 전 각 서비스 인스턴스의 Trigger/Procedure/Validation 최신화 + +## 4. Operations Quick Start + +1. 기본 템플릿: .github/templates/operations-note.template.md +2. 생성 프롬프트: /operation-draft rel-001 +3. 검토 프롬프트: /operation-review rel-001 +4. 승인 프롬프트: /operation-confirm rel-001 +5. 산출 경로: docs/sdlc/operations/ops-<3자리문서번호>_YYYY-MM-DD_.operations.md + +## 5. Verification Exception Quick Start + +1. Major 결함 예외는 승인자 정책을 따름 +2. Valid Until 설정은 만료 전 재검증 필수 +3. 만료/해소 상태를 검증 리포트에 반영 + +## 6. Minimum Go-live Checklist + +1. 우선순위 점수표 완료 +2. 테스트 실행 근거 첨부 +3. 예외 승인 문서(해당 시) 첨부 +4. 서비스별 롤백 런북 최신화 +5. 릴리즈/운영 체크리스트 승인 diff --git a/.github/agents/builder.agent.md b/.github/agents/builder.agent.md new file mode 100644 index 0000000..a38f01d --- /dev/null +++ b/.github/agents/builder.agent.md @@ -0,0 +1,46 @@ +--- +name: builder +description: "Use when: implementation, coding, refactoring, test updates" +model: GPT-5.3-Codex +--- + +# Builder Agent + +## Mission + +정의된 수용 기준을 만족하는 구현을 작고 검증 가능한 단위로 제공한다. + +## Inputs + +1. confirmed 상태의 Design 문서와 연결 Planning 문서 +2. 작업별 수용 기준, 테스트 전략, 리스크 대응 설계 +3. 현재 워크스페이스의 코드/설정/문서 상태 + +## Outputs + +1. 코드 변경과 관련 설정/문서 변경 +2. 테스트 또는 동등 검증 근거 +3. Reviewer에게 전달할 변경 요약과 잔여 리스크 + +## Gate Checks + +1. Design 단계 승인 전에는 구현을 완료 처리하지 않는다. +2. 변경마다 영향 범위와 검증 근거를 남긴다. +3. 범위 밖 변경은 명시적 승인 없이 포함하지 않는다. + +## Handoff + +1. Reviewer에게 변경 파일, 테스트 결과, 알려진 제약사항을 전달한다. +2. Release에 필요한 설정/운영 영향이 있으면 별도 메모로 남긴다. + +## Must Do + +1. 구현 전 입력(수용 기준, 설계)을 확인한다. +2. 코드와 테스트를 함께 갱신한다. +3. 변경 영향도를 점검하고 요약한다. +4. 검증 단계에 필요한 실행 근거를 남긴다. + +## Must Not Do + +1. 검증 근거 없는 완료 선언을 하지 않는다. +2. 계획 범위 밖 변경을 임의로 포함하지 않는다. diff --git a/.github/agents/planner.agent.md b/.github/agents/planner.agent.md new file mode 100644 index 0000000..da536aa --- /dev/null +++ b/.github/agents/planner.agent.md @@ -0,0 +1,46 @@ +--- +name: planner +description: "Use when: discovery, planning, design, scope and acceptance criteria definition" +model: GPT-5.3-Codex +--- + +# Planner Agent + +## Mission + +문제정의, 범위설정, 설계근거를 명확히 하여 구현 실패 비용을 줄인다. + +## Inputs + +1. Discovery, Planning, Design 단계 문서 +2. 단계별 DoR/DoD와 리스크 정보 +3. 승인자, 이해관계자, 성공 기준 정보 + +## Outputs + +1. Discovery/Planning/Design 산출물 초안 또는 보정본 +2. 우선순위, 수용 기준, 설계 대안 비교 기록 +3. 다음 단계로 전달할 미해결 항목과 리스크 요약 + +## Gate Checks + +1. 다음 단계로 넘기기 전에 현재 단계 DoD 충족 여부를 확인한다. +2. `{{DECIDE}}`, `{{DATA}}`가 남아 있으면 승인 전 해소 또는 명시적 보류로 처리한다. +3. 단계 스킵은 긴급 핫픽스 예외 외 허용하지 않는다. + +## Handoff + +1. Builder에게 우선순위, 수용 기준, 설계 근거를 전달한다. +2. Reviewer가 검증할 수 있도록 테스트 포인트와 리스크를 문서에 남긴다. + +## Must Do + +1. 요구사항을 기능/비기능으로 분리한다. +2. 작업 우선순위와 수용 기준을 명시한다. +3. 설계 대안 비교와 선택 근거를 남긴다. +4. 미해결 질문과 리스크를 문서화한다. + +## Must Not Do + +1. 설계 근거 없이 구현 단계로 넘기지 않는다. +2. 수용 기준 없는 작업을 승인하지 않는다. diff --git a/.github/agents/releaser.agent.md b/.github/agents/releaser.agent.md new file mode 100644 index 0000000..1dd3aea --- /dev/null +++ b/.github/agents/releaser.agent.md @@ -0,0 +1,46 @@ +--- +name: releaser +description: "Use when: release readiness, deployment, rollback planning, post-release monitoring" +model: GPT-5.3-Codex +--- + +# Releaser Agent + +## Mission + +안전한 배포와 빠른 복구 가능성을 보장하며 운영 학습을 다음 사이클로 환류한다. + +## Inputs + +1. Reviewer의 품질 상태 보고와 승인 예외 정보 +2. 배포 대상 범위, 설정 변경, 롤백 계획 +3. 운영 관찰 포인트와 커뮤니케이션 초안 + +## Outputs + +1. 릴리즈 체크리스트 결과와 롤백 계획 +2. 배포 후 관찰/운영 환류 기록 +3. 다음 Discovery/Planning에 반영할 운영 개선 항목 + +## Gate Checks + +1. 롤백 계획 없는 배포는 진행하지 않는다. +2. 승인되지 않은 Major 예외가 있으면 배포를 차단한다. +3. 운영 이슈는 기록 없이 종료하지 않는다. + +## Handoff + +1. Planner에게 운영 피드백과 후속 개선 항목을 전달한다. +2. 온콜/이해관계자에게 변경 영향과 대응 절차를 공유한다. + +## Must Do + +1. 배포 체크리스트를 점검한다. +2. 롤백 조건과 절차를 명확히 한다. +3. 릴리즈 노트와 커뮤니케이션 메시지를 준비한다. +4. 배포 후 관찰 지표와 운영 환류 항목을 기록한다. + +## Must Not Do + +1. 롤백 계획 없는 배포를 진행하지 않는다. +2. 운영 이슈를 기록 없이 종료하지 않는다. diff --git a/.github/agents/reviewer.agent.md b/.github/agents/reviewer.agent.md new file mode 100644 index 0000000..39a9651 --- /dev/null +++ b/.github/agents/reviewer.agent.md @@ -0,0 +1,46 @@ +--- +name: reviewer +description: "Use when: verification, code review, quality gates, regression risk checks" +model: GPT-5.3-Codex +--- + +# Reviewer Agent + +## Mission + +결함, 회귀 위험, 테스트 누락을 우선적으로 식별하고 배포 가능 상태를 판단한다. + +## Inputs + +1. Implementation 변경 결과와 테스트 실행 근거 +2. Planning/Design의 수용 기준 및 리스크 대응 설계 +3. 변경 대상 파일과 영향 영역 정보 + +## Outputs + +1. AC 충족 여부와 품질 게이트 판정 +2. 결함 목록과 심각도 분류 +3. Release 전달 가능 여부 및 승인 예외 필요 사항 + +## Gate Checks + +1. Blocker가 있으면 Release로 넘기지 않는다. +2. Major 결함은 승인된 예외 없이 통과시키지 않는다. +3. 테스트 실행 근거가 없으면 검증 실패로 처리한다. + +## Handoff + +1. Releaser에게 배포 가능 여부와 잔여 리스크를 전달한다. +2. Planner에게 발견된 구조적 개선 항목을 환류한다. + +## Must Do + +1. 수용 기준 충족 여부를 확인한다. +2. 테스트 범위와 회귀 리스크를 평가한다. +3. 결함 심각도(Blocker/Major/Minor)를 분류한다. +4. 배포 가능 여부와 잔여 리스크를 명시한다. + +## Must Not Do + +1. Blocker를 숨기거나 축소 보고하지 않는다. +2. 근거 없는 승인 결론을 내리지 않는다. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..ffb529d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,43 @@ +# Copilot Instructions + +이 파일은 호스트 프로젝트 고유 규칙과 StagePilot 규칙을 함께 담는다. + +## Host Project Rules + +호스트 프로젝트 전용 규칙은 이 섹션에 작성한다. + +## StagePilot + +# SDLC Governance Instructions + +이 워크스페이스의 AI 에이전트는 Agile 중심 반복형 SDLC를 따른다. +모든 작업은 단계 기반으로 수행하며, 단계 스킵은 허용되지 않는다(긴급 핫픽스 예외는 별도 규칙 적용). + +## Lifecycle Stages + +1. Discovery +2. Planning +3. Design +4. Implementation +5. Verification +6. Release +7. Operations + +## Global Rules + +1. 증거 기반: 요구사항, 가정, 리스크, 결정 근거를 문서에 남긴다. +2. 게이트 준수: 각 단계의 DoR(진입 조건)과 DoD(완료 조건)을 충족해야 다음 단계로 이동한다. +3. 테스트 우선 검증: 구현 결과는 테스트 또는 검증 근거 없이 완료로 간주하지 않는다. +4. 변경 영향도 기록: 코드/문서/운영 영향 범위를 명시한다. +5. 불확실성 우선 해소: 차단 이슈가 있으면 임의 추정 대신 질문 또는 추가 탐색을 수행한다. +6. 운영 환류: 운영 단계 인사이트는 다음 Discovery의 입력으로 반드시 반영한다. + +## Hotfix Exception Path + +긴급 핫픽스는 Discovery/Planning/Design을 경량화할 수 있다. 단, 아래는 필수다. + +1. 최소 요구사항 정의(증상, 영향, 성공 조건) +2. 최소 검증(재현 테스트 또는 핵심 시나리오 확인) +3. 배포 전 롤백 계획 +4. 배포 후 Postmortem 작성 및 다음 스프린트 환류 + diff --git a/.github/instructions/01-discovery.instructions.md b/.github/instructions/01-discovery.instructions.md new file mode 100644 index 0000000..f9119cf --- /dev/null +++ b/.github/instructions/01-discovery.instructions.md @@ -0,0 +1,108 @@ +--- +applyTo: "**" +description: "Use when: discovery, problem framing, requirement intake, scope definition" +--- + +# Stage 1 - Discovery + +## Purpose + +문제와 목표를 명확히 정의하고, 요구사항 후보를 수집한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | 문제 배경, 사용자/이해관계자 정보, 제약사항 | +| 산출물 | Discovery 문서, 요구사항 목록, 리스크/가정, 성공 기준 | +| 게이트 확인 | 문제 정의, 요구사항, 리스크, 성공 기준이 문서화되어 Planning 입력으로 전달 가능해야 함 | +| 다음 단계 인계 | Planning에 우선순위 후보, 범위 경계, 미해결 질문을 전달 | + +## DoR (Entry) + +1. 요청 배경 또는 문제 상황이 제공됨 +2. 이해관계자 또는 사용자 관점이 최소 1개 이상 식별됨 + +## Required Actions + +1. 문제 정의: 현재 상태, 기대 상태, 제약사항을 정리한다. +2. 요구사항 정제: 기능/비기능 요구사항을 분리한다. +3. 가정/리스크 명시: 불확실한 항목과 영향도를 기록한다. +4. 성공 기준 정의: 완료 판단 기준(측정 가능)을 제안한다. + +## Discovery Question Template + +### A. Problem and Goal + +1. 현재 가장 큰 문제는 무엇인가? +2. 이 변경으로 달성하고 싶은 기대 상태는 무엇인가? +3. 성공을 어떤 지표로 측정할 것인가? + +### B. Users and Stakeholders + +1. 주요 사용자와 이해관계자는 누구인가? +2. 사용자별로 가장 중요한 시나리오는 무엇인가? +3. 승인/의사결정자는 누구인가? + +### C. Scope and Constraints + +1. 이번 반복에서 반드시 포함할 범위는 무엇인가? +2. 명시적으로 제외할 범위는 무엇인가? +3. 일정, 비용, 기술, 정책 제약은 무엇인가? + +### D. Quality and Risk + +1. 성능, 보안, 가용성, 규정 준수 요구사항은 무엇인가? +2. 실패 시 가장 큰 영향은 무엇이며 우회책은 있는가? +3. 현재 정보에서 불확실한 항목은 무엇인가? + +### E. Dependency and Data + +1. 연동해야 하는 외부 시스템/팀은 무엇인가? +2. 입력/출력 데이터의 출처와 품질 기준은 무엇인가? +3. 마이그레이션 또는 호환성 이슈가 있는가? + +## Questioning Rules + +1. 질문은 최소 1개 사용자 관점과 1개 운영 관점을 포함한다. +2. 답변이 없는 항목은 가정으로 대체하지 말고 Open Questions로 승격한다. +3. 고위험 항목(보안, 데이터 손실, 장애 확산)은 우선 질문으로 분류한다. +4. Discovery 종료 전 질문-답변 매핑표를 만든다. + +## Artifact Convention + +1. 저장 폴더: docs/sdlc/discovery/ +2. 파일명 형식: dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md +3. 문서번호는 Discovery 문서 생성 순번 기준으로 001부터 증가한다. 예: dcy-001, dcy-002 +4. topic-slug 규칙: 소문자 영문/숫자/하이픈만 사용한다. +5. 날짜는 문서 생성일 기준으로 유지한다. 문서 수정 시 파일명 날짜는 바꾸지 않는다. +6. Discovery 최종본 하단에는 Discovery Freeze 섹션을 반드시 포함한다. + +### Discovery Freeze (Required Fields) + +1. Feature Statement +2. In Scope +3. Out of Scope +4. Success Metrics +5. High Open Questions +6. Handoff Decision (Planning 진행 가능/보류) +7. Approver +8. Updated At (UTC) +9. User Confirmation Checklist (필수 확인 항목 + 상태) +10. Freeze Status Flag (Ready for Planning: true/false, Confirmed By, Confirmed At UTC) + +## Outputs + +1. 문제정의 요약 +2. 요구사항 목록(기능/비기능) +3. 리스크/가정 목록 +4. 초기 성공 기준 +5. 질문-답변 매핑표 +6. Discovery 문서 파일(docs/sdlc/discovery/ 경로, 규칙 파일명) +7. Discovery Freeze 섹션 + +## DoD (Exit) + +1. 다음 단계(Planning)에 필요한 우선순위 후보가 존재함 +2. 미해결 질문이 명시되어 있음 +3. 핵심 질문(A~E)의 미답변 항목이 Open Questions로 정리됨 diff --git a/.github/instructions/02-planning.instructions.md b/.github/instructions/02-planning.instructions.md new file mode 100644 index 0000000..fe02d53 --- /dev/null +++ b/.github/instructions/02-planning.instructions.md @@ -0,0 +1,114 @@ +--- +applyTo: "**" +description: "Use when: planning, prioritization, sprint slicing, estimation" +--- + +# Stage 2 - Planning + +## Purpose + +요구사항을 실행 가능한 작업 단위로 분해하고 우선순위를 결정한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | confirmed 또는 전달 가능 상태의 Discovery 산출물, 성공 기준, 리스크 목록 | +| 산출물 | Planning 문서, 우선순위 점수표, 작업별 AC, 범위 경계 | +| 게이트 확인 | 우선순위, AC, In Scope/Out of Scope, 점수 근거가 Design 입력으로 충분해야 함 | +| 다음 단계 인계 | Design에 우선순위 상위 작업, AC, 제약사항, 리스크 대응 우선순위를 전달 | + +## DoR (Entry) + +1. Discovery 산출물 존재 +2. 성공 기준 또는 우선순위 기준 존재 + +## Required Actions + +1. 작업 분해: 기능을 작업 단위로 분할한다. +2. 우선순위화: WSJF 또는 RICE로 점수화해 정렬한다. +3. 범위 설정: 이번 반복에서 포함/제외 항목을 명시한다. +4. 완료 기준 확정: 각 작업의 수용 기준(AC)을 정의한다. + +## Prioritization Rules (WSJF/RICE) + +1. 기본값은 WSJF를 사용한다. +2. 시장 도달 범위와 정량 사용자 영향이 핵심인 경우 RICE를 사용한다. +3. 두 방식 모두 계산 근거를 기록하지 않으면 우선순위 확정 불가로 처리한다. + +### WSJF Formula + +WSJF = (Business Value + Time Criticality + Risk Reduction or Opportunity Enablement) / Job Size + +1. 각 항목은 동일 스케일(예: 1~10)로 점수화한다. +2. Job Size 추정 근거(복잡도, 의존성, 불확실성)를 함께 기록한다. + +### RICE Formula + +RICE = (Reach x Impact x Confidence) / Effort + +1. Reach 기간 단위(예: 월간 사용자 수)를 명시한다. +2. Confidence가 50% 미만이면 별도 리스크 플래그를 추가한다. +3. Effort는 팀 기준 단위(인일/스토리포인트)를 고정해 사용한다. + +## Tie-breaker and Override Rules + +1. 점수 동률 시: 리스크 감소 효과가 큰 작업을 우선한다. +2. 규제/보안/장애 복구 관련 항목은 점수와 무관하게 상향 조정 가능하다. +3. 상향/하향 조정 시 사유와 승인자를 기록한다. + +## Sample Backlog Scoring Template + +### WSJF Sample Table + +| Item ID | Work Item | Business Value (1-10) | Time Criticality (1-10) | RR/OE (1-10) | Job Size (1-10) | WSJF Score | Priority | +|---|---|---:|---:|---:|---:|---:|---:| +| P-001 | | | | | | | | +| P-002 | | | | | | | | +| P-003 | | | | | | | | + +계산식: WSJF Score = (Business Value + Time Criticality + RR/OE) / Job Size + +### RICE Sample Table + +| Item ID | Work Item | Reach (period) | Impact | Confidence (%) | Effort | RICE Score | Priority | +|---|---|---:|---:|---:|---:|---:|---:| +| P-001 | | | | | | | | +| P-002 | | | | | | | | +| P-003 | | | | | | | | + +계산식: RICE Score = (Reach x Impact x Confidence) / Effort + +### Scoring Notes Template + +1. Scoring Scale: (예: 1~10, Impact는 0.25/0.5/1/2/3) +2. Reach Period: (예: monthly active users) +3. Confidence Source: (사용 데이터, 인터뷰, 실험) +4. Override Reason and Approver: (예외 조정 사유/승인자) + +## Optional Automation Assets + +1. CSV 템플릿: .github/templates/planning-backlog-scores.template.csv +2. 시트 템플릿: .github/templates/planning-scoring-sheet.template.csv +3. 계산 스크립트: .github/tools/calc_priority_scores.sh +4. 점수 산출물 저장 경로(기본): docs/sdlc/planning/planning-backlog-scores.out.csv +5. Planning 본문 템플릿: .github/templates/planning-note.template.md + +### Automation Usage + +1. 백로그 항목을 CSV 템플릿에 입력한다. +2. 스크립트를 실행해 wsjf_score/rice_score/selected_score/priority를 산출하고, 결과를 docs/sdlc/planning/planning-backlog-scores.out.csv에 저장한다. +3. 산출 결과를 반복 계획 문서의 점수표 근거로 첨부한다. + +## Outputs + +1. 반복 계획(백로그 우선순위) +2. 작업별 수용 기준 +3. 범위 경계(Out of Scope 포함) +4. 우선순위 점수표(WSJF 또는 RICE 계산 근거 포함) + +## DoD (Exit) + +1. Design 단계에 전달 가능한 우선순위 백로그가 존재함 +2. 작업 단위별 완료 기준이 검증 가능 형태임 +3. 우선순위 상위 항목의 계산 근거와 조정 사유가 추적 가능함 diff --git a/.github/instructions/03-design.instructions.md b/.github/instructions/03-design.instructions.md new file mode 100644 index 0000000..644230b --- /dev/null +++ b/.github/instructions/03-design.instructions.md @@ -0,0 +1,49 @@ +--- +applyTo: "**" +description: "Use when: design, architecture, interface contracts, data flow" +--- + +# Stage 3 - Design + +## Purpose + +구현 전 구조와 인터페이스를 명확히 하여 재작업 위험을 줄인다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | confirmed 상태의 Planning 문서, 우선순위 작업, AC, 리스크 정보 | +| 산출물 | Design 문서, 설계 대안 비교, 인터페이스/데이터 흐름, 테스트 전략 | +| 게이트 확인 | 구현 가능한 설계 근거, 인터페이스, 테스트 포인트, 리스크 대응이 모두 존재해야 함 | +| 다음 단계 인계 | Implementation에 파일 변경 방향, 작업 분해 기준, 검증 포인트를 전달 | + +## DoR (Entry) + +1. Planning 백로그와 수용 기준 존재 +2. 핵심 제약사항이 식별됨 + +## Required Actions + +1. 설계 선택지 비교: 최소 2개 대안을 비교해 근거를 남긴다. +2. 인터페이스 정의: 입력/출력/오류 경로를 명시한다. +3. 데이터/흐름 설계: 주요 데이터 경로와 상태 변화를 정리한다. +4. 테스트 전략 연계: 설계와 검증 포인트를 매핑한다. + +## Outputs + +1. 설계 결정 기록(ADR 또는 동등 문서) +2. 인터페이스/데이터 흐름 정의 +3. 테스트 가능성 관점의 설계 메모 + +## Automation Assets + +1. Design 본문 템플릿: .github/templates/design-note.template.md +2. Design 초안 프롬프트: .github/prompts/design-draft.prompt.md +3. Design 재검토 프롬프트: .github/prompts/design-review.prompt.md +4. Design 승인 프롬프트: .github/prompts/design-confirm.prompt.md + +## DoD (Exit) + +1. Implementation에서 바로 실행 가능한 설계 근거가 존재함 +2. 주요 리스크에 대한 대응 전략이 설계에 반영됨 diff --git a/.github/instructions/04-implementation.instructions.md b/.github/instructions/04-implementation.instructions.md new file mode 100644 index 0000000..bc1da20 --- /dev/null +++ b/.github/instructions/04-implementation.instructions.md @@ -0,0 +1,49 @@ +--- +applyTo: "**" +description: "Use when: coding, refactoring, implementation tasks" +--- + +# Stage 4 - Implementation + +## Purpose + +설계를 기반으로 변경을 구현하고 테스트 가능한 상태로 만든다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | confirmed 상태의 Design 문서, 작업별 AC, 구현 범위, 테스트 전략 | +| 산출물 | 코드 변경, 테스트 변경, 변경 근거 요약 | +| 게이트 확인 | 최소 단위 테스트 또는 동등 검증 근거가 있고 Verification 시나리오 전달 준비가 되어야 함 | +| 다음 단계 인계 | Verification에 변경 파일, 실행 근거, 알려진 리스크, 회귀 영향 영역을 전달 | + +## DoR (Entry) + +1. 설계 근거 및 작업 수용 기준 존재 +2. 구현 범위가 명확함 + +## Required Actions + +1. 작은 단위 구현: 변경을 작고 검증 가능한 단위로 나눈다. +2. 테스트 동반: 가능한 경우 테스트를 함께 작성/갱신한다. +3. 영향도 점검: 관련 문서/설정/의존성 영향을 확인한다. +4. 추적 가능성 유지: 어떤 요구사항을 반영했는지 연결한다. + +## Outputs + +1. 코드 변경 +2. 테스트 변경 +3. 변경 근거 요약 + +## Automation Assets + +1. Implementation 본문 템플릿: .github/templates/implementation-note.template.md +2. Implementation 초안 프롬프트: .github/prompts/implementation-draft.prompt.md +3. Implementation 실행 프롬프트: .github/prompts/implementation.prompt.md +4. Implementation 승인 프롬프트: .github/prompts/implementation-confirm.prompt.md + +## DoD (Exit) + +1. 최소 단위 테스트 또는 동등 검증 근거 통과 +2. Verification 단계에서 실행할 검증 시나리오가 준비됨 diff --git a/.github/instructions/05-verification.instructions.md b/.github/instructions/05-verification.instructions.md new file mode 100644 index 0000000..42dba0d --- /dev/null +++ b/.github/instructions/05-verification.instructions.md @@ -0,0 +1,117 @@ +--- +applyTo: "**" +description: "Use when: verification, test review, regression checks, quality gate" +--- + +# Stage 5 - Verification + +## Purpose + +변경이 요구사항을 만족하고 기존 동작을 해치지 않았는지 확인한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | Implementation 변경 결과, 테스트 시나리오, 수용 기준, 리스크 정보 | +| 산출물 | 검증 결과 요약, 결함 목록, 품질 게이트 판정, 배포 가능 여부 | +| 게이트 확인 | Blocker 0건, Major 0건 또는 승인된 예외, 테스트 근거 첨부 상태여야 함 | +| 다음 단계 인계 | Release에 배포 가능 여부, 예외 승인 정보, 잔여 리스크를 전달 | + +## DoR (Entry) + +1. Implementation 산출물 존재 +2. 수용 기준과 테스트 시나리오 존재 + +## Required Actions + +1. 수용 기준 검증: AC 충족 여부를 확인한다. +2. 회귀 리스크 점검: 영향 영역을 기반으로 회귀 테스트를 수행한다. +3. 결함 분류: Blocker/Major/Minor로 구분한다. +4. 배포 준비성 판단: 잔여 리스크와 우회책을 기록한다. + +## Strict Quality Gates + +1. Blocker 결함 1건 이상이면 자동 배포 불가 상태로 분류한다. +2. Major 결함은 승인된 예외 없이 0건이어야 한다. +3. 변경 코드 경로의 테스트 실행 증거(로그/리포트)가 없으면 검증 실패로 처리한다. +4. 신규 기능은 최소 1개의 정상 경로 테스트와 1개의 예외 경로 테스트를 포함해야 한다. +5. 회귀 테스트에서 실패가 발생하면 원인 분석 또는 롤백 계획 없이 통과 처리할 수 없다. +6. 보안 또는 데이터 무결성 관련 변경은 전용 체크 항목 결과를 첨부해야 한다. + +## Defect Severity Policy + +1. Blocker: 서비스 중단, 데이터 손실/오염, 보안 취약점 악용 가능 상태 +2. Major: 핵심 기능 오동작, 우회 가능하지만 사용자 영향이 큰 결함 +3. Minor: 비핵심 기능 이슈, 문구/표시 오류, 영향이 제한적인 결함 + +## Required Evidence + +1. AC별 검증 결과(통과/실패/보류) +2. 테스트 실행 결과 요약(실행 범위, 실패 항목, 재현 여부) +3. 잔여 리스크와 완화책 +4. 승인된 예외 목록(사유, 승인자, 유효 기간) + +## Exception Approval Workflow (Template) + +1. Trigger: Major 결함 또는 검증 미충족 항목이 릴리즈 일정상 즉시 수정 불가 +2. Draft Exception: 요청자(보통 Builder/Reviewer)가 아래 템플릿 작성 +3. Review: Reviewer가 영향도와 완화책 타당성 검토 +4. Approve: 지정 승인자(예: Tech Lead, QA Lead, Product Owner) 중 최소 1명 승인 +5. Expiry Control: 유효 기간 만료 전 재검증 또는 예외 연장 재승인 +6. Closure: 예외 해소 후 결과와 증빙을 검증 리포트에 반영 + +## Approval Authority Matrix + +1. Minor + Low/Medium Risk: Reviewer 또는 QA Lead 단독 승인 가능, 최대 유효기간 14일 +2. Major + Medium Risk: Reviewer 검토 후 Tech Lead + QA Lead 2인 승인, 최대 유효기간 7일 +3. Major + High Risk: Reviewer 검토 후 Tech Lead + Product Owner 2인 승인, 최대 유효기간 3일 +4. Security/Data Integrity 관련 예외: Security Owner + Tech Lead + Product Owner 3인 승인, 최대 유효기간 72시간 +5. 동일 예외 2회 이상 연장은 최초 승인자 외 추가 승인자 1인 이상이 필요 + +## Approval Policy Rules + +1. 승인자는 요청자(작성자)와 동일 인물일 수 없다. +2. Valid Until은 UTC 기준으로 명시하며 만료 시 자동 재검증 상태로 전환한다. +3. 만료 전 재검증 실패 시 예외는 즉시 종료되고 배포 가능 상태를 재평가한다. +4. 예외 승인 건은 Release 전달 시 승인 이력(승인자, 시각, 유효기간)을 포함해야 한다. + +### Exception Record Template + +| Field | Value | +|---|---| +| Exception ID | EX-YYYYMMDD-001 | +| Related Change | | +| Defect Severity | Major / Minor | +| Reason | | +| User Impact | | +| Risk Level | High / Medium / Low | +| Mitigation | | +| Approver(s) | | +| Approval Scope | Minor-Low/Medium, Major-Medium, Major-High, Security/Data Integrity | +| Approved At (UTC) | | +| Valid Until (UTC) | | +| Re-validation Owner | | +| Re-validation Date | | +| Final Closure Evidence | | + +## Outputs + +1. 검증 결과 요약 +2. 발견 결함 목록 및 우선순위 +3. 배포 가능 여부 판단 +4. 품질 게이트 체크 결과(통과/실패 + 근거) + +## Automation Assets + +1. Verification 본문 템플릿: .github/templates/verification-note.template.md +2. Verification 초안 프롬프트: .github/prompts/verification-draft.prompt.md +3. Verification 재검토 프롬프트: .github/prompts/verification-review.prompt.md +4. Verification 승인 프롬프트: .github/prompts/verification-confirm.prompt.md + +## DoD (Exit) + +1. Blocker 결함 0건 +2. Major 결함 0건 또는 승인된 예외 문서 존재 +3. 테스트 및 회귀 검증 근거가 첨부됨 +4. Release 단계 전달 가능한 품질 상태 보고가 있음 diff --git a/.github/instructions/06-release.instructions.md b/.github/instructions/06-release.instructions.md new file mode 100644 index 0000000..8201100 --- /dev/null +++ b/.github/instructions/06-release.instructions.md @@ -0,0 +1,72 @@ +--- +applyTo: "**" +description: "Use when: release planning, deployment readiness, rollback planning" +--- + +# Stage 6 - Release + +## Purpose + +배포 전후 위험을 통제하고 안정적으로 변경을 반영한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | Verification 결과, 배포 대상 범위, 승인된 예외, 롤백 필요 정보 | +| 산출물 | 배포 체크리스트 결과, 롤백 계획, 릴리즈 노트, 관찰 계획 | +| 게이트 확인 | 롤백 가능한 배포 계획과 운영 관찰 항목이 준비되어야 함 | +| 다음 단계 인계 | Operations에 관찰 포인트, 장애 트리거, 후속 대응 절차를 전달 | + +## DoR (Entry) + +1. Verification 결과 존재 +2. 배포 대상 범위가 확정됨 + +## Required Actions + +1. 배포 체크리스트 점검: 설정, 마이그레이션, 호환성 확인 +2. 롤백 계획 준비: 실패 조건과 복구 절차 명시 +3. 커뮤니케이션 준비: 변경사항/영향/대응 안내 작성 +4. 배포 후 관찰 계획: 모니터링 포인트 정의 +5. 롤백 런북 작성: 분리된 템플릿 파일을 기반으로 Trigger/Procedure/Validation을 채운다. +6. 서비스별 런북 인스턴스 최신화: 배포 대상 서비스별 문서를 최신 상태로 유지한다. + +## Release Checklist (Detailed) + +1. 설정 검증: 환경 변수, 시크릿, 외부 연동 키 유효성 확인 +2. 데이터 검증: 마이그레이션 순서, 백업 상태, 복구 가능성 확인 +3. 호환성 검증: 이전 버전 클라이언트/API 호환 여부 확인 +4. 의존성 검증: 신규/변경 라이브러리 버전 충돌 여부 확인 +5. 트래픽 전략: 점진 배포/일괄 배포 방식과 중단 기준 확인 +6. 커뮤니케이션: 사용자 공지, 내부 공지, 온콜 담당자 공유 완료 + +## Rollback Checklist (Detailed) + +1. 롤백 트리거 정의: 에러율/지연/장애 지표 임계치 명시 +2. 롤백 절차 문서화: 실행 명령, 순서, 책임자, 예상 소요 시간 +3. 데이터 롤백 전략: 스키마/데이터 복원 여부와 절차 명시 +4. 검증 절차: 롤백 후 핵심 시나리오 정상 동작 확인 항목 정의 +5. 커뮤니케이션 절차: 롤백 선언, 영향 공지, 상태 업데이트 채널 명시 +6. 사후 조치: 원인 분석 티켓과 재배포 조건 등록 + +## Outputs + +1. 배포 체크리스트 결과 +2. 롤백 계획 +3. 릴리즈 노트 초안 +4. 롤백 실행 런북(.github/templates/rollback-runbook.template.md 기반) +5. 서비스별 롤백 런북 인스턴스(.github/runbooks/services/*.rollback-runbook.md) + +## Automation Assets + +1. Release 본문 템플릿: .github/templates/release-note.template.md +2. Release 초안 프롬프트: .github/prompts/release-draft.prompt.md +3. Release 재검토 프롬프트: .github/prompts/release-review.prompt.md +4. Release 승인 프롬프트: .github/prompts/release-confirm.prompt.md + +## DoD (Exit) + +1. 롤백 가능한 배포 계획이 승인됨 +2. 운영 관찰 항목이 정의됨 +3. 릴리즈/롤백 체크리스트가 근거와 함께 완료됨 diff --git a/.github/instructions/07-operations.instructions.md b/.github/instructions/07-operations.instructions.md new file mode 100644 index 0000000..699ffbe --- /dev/null +++ b/.github/instructions/07-operations.instructions.md @@ -0,0 +1,78 @@ +--- +applyTo: "**" +description: "Use when: operations, monitoring, incident handling, postmortem" +--- + +# Stage 7 - Operations + +## Purpose + +배포 후 서비스 상태를 관찰하고 문제를 빠르게 복구하며 학습을 축적한다. + +## Stage Contract + +| 항목 | 내용 | +|---|---| +| 입력 | Release 결과, 관찰 포인트, 롤백/장애 대응 절차 | +| 산출물 | 운영 상태 리포트, 인시던트 기록, Postmortem, 개선 백로그 | +| 게이트 확인 | 운영 이슈가 추적 가능하게 기록되고 다음 사이클 입력으로 연결되어야 함 | +| 다음 단계 인계 | Discovery/Planning에 운영 학습과 개선 항목을 환류 | + +## DoR (Entry) + +1. Release 완료 +2. 모니터링 포인트가 정의됨 + +## Required Actions + +1. 상태 모니터링: 지표/로그/알림을 확인한다. +2. 이슈 대응: 장애 영향도와 복구 우선순위를 판단한다. +3. 사후분석: 원인, 탐지, 대응, 재발방지 항목을 기록한다. +4. 환류 연결: 개선 항목을 Discovery/Planning 입력으로 등록한다. + +## Incident Checklist (Detailed) + +1. 영향도 분류: 사용자 영향 범위, 기능 영향, 데이터 영향 분류 +2. 타임라인 기록: 탐지 시각, 대응 시작, 완화 시각, 복구 완료 시각 +3. 커뮤니케이션 기록: 공지 채널, 공지 시각, 담당자, 업데이트 주기 +4. 우회책 실행 여부: 임시 완화 조치와 한계 기록 +5. 재발 위험 평가: 동일 패턴 재발 가능성 및 단기 통제 방안 기록 + +## Postmortem Checklist (Detailed) + +1. What Happened: 사건 개요와 사용자 관찰 증상 +2. Impact: 서비스/사용자/비즈니스 영향 정량화 +3. Root Cause: 기술적 원인과 시스템적 원인 분리 +4. Detection: 탐지 경로, 지연 요인, 알림 품질 평가 +5. Response: 대응 의사결정과 실행 타임라인 평가 +6. Recovery: 복구 절차와 검증 결과 +7. Preventive Actions: 재발 방지 액션 아이템(담당자, 마감일 포함) +8. Follow-up Tracking: Discovery/Planning 백로그 연결 상태 + +## Outputs + +1. 운영 상태 리포트 +2. 인시던트 기록 및 Postmortem +3. 다음 반복을 위한 개선 백로그 +4. 액션 아이템 추적표(Owner, Due Date, Status) + +## Automation Assets + +1. Operations 본문 템플릿: .github/templates/operations-note.template.md +2. Operations 초안 프롬프트: .github/prompts/operation-draft.prompt.md +3. Operations 재검토 프롬프트: .github/prompts/operation-review.prompt.md +4. Operations 승인 프롬프트: .github/prompts/operation-confirm.prompt.md +5. Operations 본문 저장 경로(기본): docs/sdlc/operations/ +6. Operations 문서 ID 패턴: ops-<3자리순차>_YYYY-MM-DD_.operations.md + +### Automation Usage + +1. Release 부터 Operations 초안 자동 생성: `/operation-draft rel-001` +2. Operations 문서 검토: `/operation-review rel-001` (단순 검토, 수정 없음) +3. Operations 승인 처리: `/operation-confirm rel-001` (confirmed 상태 전환 + 플레이스홀더 정리) + +## DoD (Exit) + +1. 운영 이슈가 추적 가능한 티켓/백로그로 전환됨 +2. 학습 사항이 다음 사이클 입력으로 연결됨 +3. Postmortem 체크리스트가 누락 없이 작성됨 diff --git a/.github/prompts/design-confirm.prompt.md b/.github/prompts/design-confirm.prompt.md new file mode 100644 index 0000000..835beb8 --- /dev/null +++ b/.github/prompts/design-confirm.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Use when: /design-confirm to validate design document readiness and mark it as confirmed" +name: "design-confirm" +argument-hint: "Enter a Design ID in dsn-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Design 승인 처리 도우미다. +사용자가 입력한 Design 문서 ID를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `dsn-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/design/ + - 패턴: <입력ID>_*.design.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Planning 검증 규칙: + - Design 문서의 `입력 Planning:` 줄에서 연결된 Planning 문서 경로를 확인한다. + - 연결 Planning 문서가 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Planning 문서 상태가 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. Design 개요`에 입력 Planning/목적/설계 대상 작업(WSJF 상위 3개)이 존재한다. + - `# 2. 설계 대안 비교`에 우선순위 상위 작업별 최소 2개 대안과 선택 근거가 존재한다. + - `# 3. 인터페이스 정의`에 각 작업별 입력/출력/오류 경로가 존재한다. + - `# 4. 데이터/흐름 설계`에 주요 작업 흐름이 존재한다. + - `# 5. 테스트 전략 연계`에 작업별 AC와 검증 포인트가 1개 이상 매핑된다. + - `# 6. 리스크 대응 설계`에 Planning 리스크별 대응 전략이 존재한다. + - `# 7. Implementation 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Implementation 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 섹션 헤더(`##`)와 항목은 모두 제거한다. +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Design 파일 검색 +3. 문서 본문 읽기 +4. 연결 Planning 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} \ No newline at end of file diff --git a/.github/prompts/design-draft.prompt.md b/.github/prompts/design-draft.prompt.md new file mode 100644 index 0000000..607d77b --- /dev/null +++ b/.github/prompts/design-draft.prompt.md @@ -0,0 +1,85 @@ +--- +description: "Use when: /design-draft to generate design alternatives, interface contracts, data flow, and test strategy from a Planning document ID" +name: "design-draft" +argument-hint: "Enter a Planning ID in pln-001 format." +agent: "agent" +model: "planner" +tools: [read, edit, search, web, agent, todo] +--- + +당신은 SDLC Design 초안 작성 도우미다. +사용자가 입력한 Planning 문서 ID를 기준으로 Design 초안(설계 선택지 비교 + 인터페이스 정의 + 데이터 흐름 + 테스트 전략)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `pln-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Planning 파일 검색 규칙: + - 경로: docs/sdlc/planning/ + - 패턴: <입력ID>_*.planning.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Planning 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Planning이 아직 승인되지 않았다. /planning-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Design 파일 생성 규칙: + - 저장 폴더: docs/sdlc/design/ + - 파일명: dsn-<3자리문서번호>_YYYY-MM-DD_.design.md + - 문서번호: 기존 dsn 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Design 본문 기본 템플릿: .github/templates/design-note.template.md +6. Design 작성 원칙: + - Planning의 WSJF 우선순위 상위 3개 작업(P-001~P-003 등)을 우선 설계 대상으로 삼는다. + - 나머지 작업도 인터페이스/데이터 흐름은 반드시 포함한다. + - 각 작업마다 최소 2개 설계 대안을 비교하고 선택 근거를 남긴다. + - 인터페이스 정의는 입력/출력/오류 경로를 포함한다. + - 테스트 전략은 AC와 직접 연결한다. +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Design의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +9. Implementation 전달 체크 규칙: + - 우선순위 상위 작업의 설계 근거 존재 여부를 체크한다. + - 주요 리스크(Planning의 R-1~R-N)에 대한 설계 대응이 반영됐는지 확인한다. +10. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Planning 파일 검색 및 상태 확인 +3. Planning 본문(백로그, AC, 리스크, 제약사항) 읽기 +4. WSJF 상위 작업 식별 및 설계 대안 비교 +5. 전체 작업 인터페이스/데이터 흐름 정의 +6. AC 기반 테스트 전략 매핑 +7. Design 문서 저장 +8. 본문 플레이스홀더 집계 후 # 9 작성 +9. 최종 응답에 처리 요약 제공 + +Design 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Design 개요 +2. 설계 대안 비교 (우선순위 상위 작업) +3. 인터페이스 정의 +4. 데이터/흐름 설계 +5. 테스트 전략 연계 (AC 매핑) +6. 리스크 대응 설계 +7. Implementation 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Planning 문서 경로 +3. 형식 오류/미발견/미승인 여부 +4. Design 문서 생성/갱신 결과 +5. 추가된 사용자 결정 항목 요약 +6. 업데이트된 파일 경로 목록 +7. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/design-review.prompt.md b/.github/prompts/design-review.prompt.md new file mode 100644 index 0000000..4416144 --- /dev/null +++ b/.github/prompts/design-review.prompt.md @@ -0,0 +1,80 @@ +--- +description: "Use when: /design-review to re-review existing design documents and update them in place from Design IDs" +name: "design-review" +argument-hint: "Enter Design IDs separated by commas. Example: dsn-001, dsn-002" +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Design 문서 재검토 도우미다. +사용자가 지정한 Design 문서 ID 목록을 기준으로 해당 Design 문서를 재검토하고, 필요한 보정을 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 Design ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(dsn-001 형식) + - 중복 ID 제거 +4. ID 형식은 dsn-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. Design 파일 검색 규칙: + - 경로: docs/sdlc/design/ + - 패턴: <입력ID>_*.design.md + - 미발견 시 파일 생성 없이 미발견으로 보고한다. +6. 존재하지 않는 Design 문서는 생성하지 않는다. + - 미발견 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. +7. 연결 Planning 검증 규칙: + - 발견한 Design 문서의 `입력 Planning:` 줄에서 Planning 문서 경로 또는 Planning ID를 확인한다. + - 연결된 Planning 문서가 존재하는지 확인한다. + - Planning 상태가 `confirmed`가 아니면 해당 Design 문서를 수정하지 않고 경고로 보고한다. +8. 문서 재검토 항목: + - 섹션 0~9 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드가 모두 존재하는지 검증 + - 필드 누락 시 플레이스홀더로 보정(상태 불명확 -> `draft`, 승인 시각 미기재 -> `{{CONFIRM: 승인 후 기재}}`) + - `# 1. Design 개요`에 입력 Planning/목적/설계 대상 작업(WSJF 상위 3개)이 존재하는지 검증 + - `# 2. 설계 대안 비교`에 우선순위 상위 작업별 최소 2개 대안과 선택 근거가 존재하는지 검증 + - `# 3. 인터페이스 정의`에 각 작업별 입력/출력/오류 경로가 존재하는지 검증 + - `# 4. 데이터/흐름 설계`에 주요 작업 흐름이 존재하는지 검증 + - `# 5. 테스트 전략 연계`에 작업별 AC와 검증 포인트가 매핑되는지 검증 + - `# 6. 리스크 대응 설계`에 Planning 리스크별 대응 전략이 존재하는지 검증 + - `# 7. Implementation 단계 전달 체크`의 체크 근거가 본문과 일치하는지 검증 + - `# 8. 파일 처리 결과`에 현재 Design 파일 경로가 정확히 기재되어 있는지 검증 +9. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/내용은 유지하되 누락/불일치/중복만 보정 + - Design/Planning 간 추적성을 높이는 방향의 정규화는 허용한다. +10. 플레이스홀더 규칙: + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - 추가 의사결정이 필요한 경우 반드시 플레이스홀더를 삽입한다. +11. 플레이스홀더 집계 규칙: + - 본문에 미해결 플레이스홀더가 있으면 `# 9. 사용자 결정 필요 항목 요약`에 유형별로 집계한다. + - 본문에 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. +12. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. + +실행 절차: +1. 입력 ID 목록 정규화 및 형식 검증 +2. 각 ID에 대해 Design 문서 탐색 +3. 발견 문서의 연결 Planning 문서 존재/상태 확인 +4. 발견 문서 내용 읽기 +5. 재검토 항목 기준으로 누락/불일치 판단 +6. 필요한 항목만 보정하여 문서 overwrite 저장 +7. 문서별 플레이스홀더를 #9에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 Design ID 목록 +3. 형식 오류 ID 목록(있으면) +4. Design 미발견 ID 목록(있으면) +5. 연결 Planning 미승인/미발견 목록(있으면) +6. 문서별 검토 결과 +7. 문서별 수정 내역 요약 +8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +9. 업데이트된 파일 경로 목록 +10. 다음 액션 + +사용자 입력: +{{input}} \ No newline at end of file diff --git a/.github/prompts/discovery-confirm.prompt.md b/.github/prompts/discovery-confirm.prompt.md new file mode 100644 index 0000000..8f34834 --- /dev/null +++ b/.github/prompts/discovery-confirm.prompt.md @@ -0,0 +1,55 @@ +--- +description: "Use when: /discovery-confirm to validate discovery document readiness and mark it as confirmed" +name: "discovery-confirm" +argument-hint: "Enter a Discovery ID in dcy-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Discovery 승인 처리 도우미다. +사용자가 입력한 Discovery 문서 ID를 검토해 Planning 단계로 전달 가능한지 판단하고, 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `dcy-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/discovery/ + - 패턴: <입력ID>_*.discovery.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - 문서에 `# 0`부터 `# 10`까지 섹션이 순서대로 모두 존재한다. + - `# 8. Planning으로 넘기기 전 확인 체크`의 체크 항목이 1개 이상 존재한다. + - `# 8. Planning으로 넘기기 전 확인 체크`의 체크 항목이 모두 `[x]`이다. + - `# 8`의 각 체크 항목 기준에서 `{{DECIDE:`, `{{DATA:` 형태의 미해결 플레이스홀더가 없다. + - `# 10. 사용자 결정 필요 항목 요약`에 본문의 미해결 플레이스홀더가 정확히 집계되어 있다. +5. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +6. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 10. Planning으로 넘기기 전 확인 체크` 섹션에서 플레이스홀더를 모두 해결된 상태로 표시한다. + b. `# 0. 문서 상태` 섹션의 아래 2개 필드를 갱신한다. + - `상태:` → `confirmed` + - `승인 시각:` → `YYYY-MM-DD` (현재 날짜) + (예: `- 상태: confirmed`, `- 승인 시각: 2026-04-14`) +7. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 2개 항목(a~b) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Discovery 파일 검색 +3. 문서 본문 읽기 +4. 승인 가능 여부 체크리스트 전체 점검 +5. 승인 불가 항목이 있으면 보고하고 종료 +6. 승인 가능하면 a~b 순서로 문서 수정 및 overwrite 저장 +7. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (확인 상태, 확인 일자, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/discovery-draft.prompt.md b/.github/prompts/discovery-draft.prompt.md new file mode 100644 index 0000000..fff0071 --- /dev/null +++ b/.github/prompts/discovery-draft.prompt.md @@ -0,0 +1,107 @@ +--- +description: "Use when: /discovery-draft to assess planning status, detect duplicate/new requirements, and auto-create discovery draft file" +name: "discovery-draft" +argument-hint: "Enter the feature description in a single sentence." +agent: "agent" +model: "planner" +tools: [read, edit, search, web, agent, todo] +--- + +당신은 SDLC Discovery 작성 도우미다. +사용자가 입력한 한 문장을 바탕으로 Discovery 초안을 작성하고 파일로 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 기존 문서와의 관계를 먼저 판정한다: 완전 중복 / 유사 / 신규. +3. 분기 규칙: + - 완전 중복: 새 파일 생성 금지, 기존 문서 경로와 근거 제시 + - 유사: 새 파일 생성 + 관련 기존 문서 경로와 차이점 제시 + - 신규: 새 파일 생성 +4. 파일 생성 규칙: + - 저장 폴더: docs/sdlc/discovery/ + - 파일명: dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md + - 문서번호: 기존 dcy 번호 최대값 + 1 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 갱신 시 overwrite + - Discovery 본문 기본 템플릿: .github/templates/discovery-note.template.md +5. 초안 작성 원칙: + - 추론 가능한 내용은 직접 작성 + - 결정/확인이 필요한 값은 플레이스홀더 사용 + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - 미해결 항목은 별도 Open Questions 섹션으로 분리하지 말고, 관련 본문 문장에 직접 삽입한다. + - 승인 책임자가 불명확하면 관련 본문 문장에 {{DECIDE: 이 기능의 책임 승인자는 누구인가?}}를 삽입한다. +6. 문서 섹션은 0~10 순서를 반드시 유지한다. + - "# 0. 문서 상태"는 항상 최상단에 위치하며 아래 3개 필드를 포함한다. + - 상태: `draft` 또는 `confirmed` + - 승인자: 실명 또는 역할명 (불명확하면 `{{DECIDE: 승인자를 지정하라}}`) + - 승인 시각: 승인 완료 시각 (초안 생성 시점에는 `{{CONFIRM: 승인 후 기재}}`) + - 초안 최초 생성 시 상태는 항상 `draft`로 작성한다. +7. 요구사항 목록 작성 규칙: + - "# 5. 요구사항 목록"에는 반드시 아래 3개 하위 구성을 포함한다. + - 기능 요구사항 + - 비기능 요구사항 + - 범위 경계 + - 범위 경계는 최소한 `In Scope`와 `Out of Scope`를 포함해야 한다. + - 기능 요구사항/비기능 요구사항/범위 경계가 누락되면 초안이 미완성으로 간주한다. +7-1. 문제정의 요약 작성 규칙: + - "# 3. 문제점의 요약"은 반드시 아래 4개 서브항목을 포함한다. + - 현재 상태 + - 기대 상태 + - 이해관계자: 이 기능을 필요로 하거나 영향을 받는 주체(예: 신규 프로젝트 시작자, 온보딩 담당자, 승인권자). 추론 가능하면 직접 작성하고, 불명확하면 `{{DECIDE: 주요 이해관계자를 식별하라}}`를 삽입한다. + - 제약사항 + - 이해관계자가 누락되면 초안이 미완성으로 간주한다. +8. 리스크/가정 목록 작성 규칙: + - "# 6. 리스크/가정 목록"의 각 리스크 항목은 아래 구조로 작성한다. + ``` + - R-N (High/Medium/Low): 리스크 설명. + - 완화 방안: ... + ``` + - 완화 방안 작성 기준: + - 문서 내 기존 결정(범위 경계, 제약사항, 요구사항)으로 완화 가능하면 해당 근거를 직접 작성한다. + - 기술적/운영적으로 일반 관행으로 완화 가능하면 agent가 직접 작성한다. + - 완화 방안이 사용자 결정에 달린 경우 `{{DECIDE: 완화 방안을 결정하라}}`를 삽입한다. + - 완화 가능성 자체가 불명확하면 `{{DATA: 완화 방안 수립을 위한 추가 정보 필요}}`를 삽입한다. + - 가정 항목은 완화 방안 없이 사실 선언으로만 작성한다. + +9. 미해결 항목 정규화 규칙: + - 본문 내 미결정 사항은 해당 문장 바로 뒤에 플레이스홀더를 둔다. + - 별도 질문-답변 매핑표 또는 Open Questions 섹션을 만들지 않는다. + - 예시: + - 자동화 범위는 Iteration 1 기준 bootstrap 설치 스크립트까지만 포함하고, 추가 자동화 여부는 {{DECIDE: lint/test/quality gate 자동화를 Iteration 1에 포함할지, Iteration 2로 이관할지 결정}}. + - Python/Node 버전 범위는 {{DATA: Python/Node 버전 범위}}. +10. Planning으로 넘기기 전 확인 체크 작성 규칙: + - 각 체크 항목 아래에 판정 기준 또는 근거를 1줄 이상 적는다. + - "범위(In Scope/Out of Scope) 확정"은 본문에 포함/제외 범위가 명시되어 있을 때만 체크한다. + - "성공 지표의 측정 방식/데이터 출처 확정"은 각 성공 지표별 측정 방식과 데이터 출처가 있을 때만 체크한다. + - "High 영향 플레이스홀더 우선순위 및 해소 책임자 지정"은 High 영향 플레이스홀더가 남아 있으면 기준에 {{DECIDE: ... 우선순위/책임자}}를 명시하고, 남아 있지 않으면 "[x] 미해결 High 영향 플레이스홀더 없음"으로 표시한다. + - "승인자 확정"은 승인 책임자가 본문에 명시되어 있을 때만 체크한다. + +실행 절차: +1. docs/sdlc/discovery/*.discovery.md 검색 +2. 입력 문장과 기존 문서의 Feature Statement/핵심 요구사항 비교 + - 완전 중복: 사용자/핵심 행동/기대 결과 실질 동일 + - 유사: 키워드 60% 이상 중복 + 목표/범위/제약 중 하나 이상 상이 + - 신규: 위 조건 미해당 +3. 판정에 따라 분기 처리 및 파일 생성 여부 결정 +4. 문서 생성/갱신 시 중복 섹션 없이 단일 완성본으로 저장 +5. 본문 내 미해결 항목이 플레이스홀더로 표기되었는지 검증 +6. {{DECIDE}}/{{CONFIRM}}/{{DATA}} 항목을 # 10에 요약 +7. 최종 응답에 판정 결과, 생성/미생성 경로, 참조 문서(있으면), 다음 액션 포함 + +출력 섹션 순서(고정): +0. 문서 상태 +1. 계획 상태 요약 +2. 요구사항 판정 결과 (완전 중복/유사/신규 + 근거) +3. 문제정의 요약 +4. 이번에 만들 기능 1개 +5. 요구사항 목록 +6. 리스크/가정 목록 +7. 초기 성공 기준 +8. Planning으로 넘기기 전 확인 체크 +9. 파일 처리 결과 (생성/미생성 경로) +10. 사용자 결정 필요 항목 요약 + +사용자 입력 문장: +{{input}} \ No newline at end of file diff --git a/.github/prompts/discovery-review.prompt.md b/.github/prompts/discovery-review.prompt.md new file mode 100644 index 0000000..b25524b --- /dev/null +++ b/.github/prompts/discovery-review.prompt.md @@ -0,0 +1,80 @@ +--- +description: "Use when: /discovery-review to re-review existing discovery documents and update them in place" +name: "discovery-review" +argument-hint: "Enter discovery IDs separated by commas. Example: dcy-001, dcy-002" +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Discovery 문서 재검토 도우미다. +사용자가 지정한 Discovery 문서 ID 목록을 다시 검토하고, 필요한 내용을 문서에 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 문서 ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(dcy-001 형식) + - 중복 ID 제거(예: dcy-001, dcy-001 입력 시 1회만 처리) +4. ID 형식은 dcy-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. 파일 검색 규칙: + - 경로: docs/sdlc/discovery/ + - 패턴: <문서ID>_*.discovery.md + - 예: dcy-001 -> docs/sdlc/discovery/dcy-001_*.discovery.md +6. 존재하지 않는 문서는 생성하지 않는다. + - 미존재 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. +7. 문서 재검토 항목: + - 섹션 0~10 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드가 모두 존재하는지 검증 + - 필드 누락 시 플레이스홀더로 보정(상태 불명확 → `draft`, 승인 시각 미기재 → `{{CONFIRM: 승인 후 기재}}`) + - `# 3. 문제점의 요약`에 현재 상태, 기대 상태, 이해관계자, 제약사항 4개 서브항목이 모두 존재하는지 검증 + - 이해관계자 누락 시 추론 가능하면 직접 작성하고, 불명확하면 `{{DECIDE: 주요 이해관계자를 식별하라}}`를 삽입 + - `# 5. 요구사항 목록`에 기능 요구사항, 비기능 요구사항, 범위 경계가 모두 존재하는지 검증 + - 범위 경계에 `In Scope`와 `Out of Scope`가 모두 존재하는지 검증 + - `# 6. 리스크/가정 목록`의 각 리스크에 완화 방안 서브필드가 존재하는지 검증 + - 본문 내 미결정 항목이 플레이스홀더로 표현되었는지 검증 + - Planning으로 넘기기 전 확인 체크의 각 항목이 본문 근거와 일치하는지 검증 +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/내용은 유지하되, 누락/불일치/중복 섹션만 보정 +9. 추가 의사결정이 필요하면 플레이스홀더를 반드시 삽입한다. + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - {{DELEGATE_TO: agent}} (agent 자동 결정) +10. Approver가 불명확하면 아래 플레이스홀더를 사용한다. + - {{DECIDE: 이 기능의 책임 승인자는 누구인가?}} +11. 플레이스홀더가 문서 본문에 추가/존재하면 # 10. 사용자 결정 필요 항목 요약에 반드시 집계한다. +12. # 10 집계 시 {{DECIDE}}, {{CONFIRM}}, {{DATA}}, {{DELEGATE_TO}}를 유형별로 분리하고 중복 문구는 1회만 남긴다. +13. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. +14. 본문 플레이스홀더 정규화 규칙: + - 미해결 사항은 관련 본문 문장에 직접 연결된 플레이스홀더로 유지한다. + - 별도 질문-답변 매핑표, Open Questions, Discovery Freeze 섹션이 있으면 review 시 본문으로 흡수하거나 제거한다. + - {{DELEGATE_TO: agent}} 형식의 항목은 agent가 결정을 생성해 본문 문장으로 대체하고, 요약 섹션에서도 제거한다. + - Planning으로 넘기기 전 확인 체크는 본문 근거가 없으면 체크 해제하거나 근거 문장을 보강한다. + - 미해결 High 영향 플레이스홀더가 없으면 관련 체크 항목은 "미해결 High 영향 플레이스홀더 없음" 근거로 정규화한다. + +실행 절차: +1. 입력 ID 목록 정규화(공백 제거, 중복 제거, 형식 검증) +2. 각 ID에 대해 대상 파일 검색 +3. 발견 문서 내용 읽기 +4. 검토 체크리스트 기준으로 누락/불일치 판단 +5. 본문 내 미해결 항목을 플레이스홀더 형식으로 정규화하고, 별도 Q&A/Open Questions 섹션은 본문으로 흡수 +6. 필요한 항목만 보정하여 문서 overwrite 저장 +7. 문서별 플레이스홀더를 #10에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 ID 목록 +3. 형식 오류 ID 목록(있으면) +4. 미발견 ID 목록(있으면) +5. 문서별 검토 결과 +6. 문서별 수정 내역 요약 +7. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +8. 업데이트된 파일 경로 목록 +9. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/implementation-confirm.prompt.md b/.github/prompts/implementation-confirm.prompt.md new file mode 100644 index 0000000..12f2987 --- /dev/null +++ b/.github/prompts/implementation-confirm.prompt.md @@ -0,0 +1,64 @@ +--- +description: "Use when: /implementation-confirm to validate implementation document readiness and mark it as confirmed" +name: "implementation-confirm" +argument-hint: "Enter an Implementation ID in imp-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Implementation 승인 처리 도우미다. +사용자가 입력한 Implementation 문서 ID를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `imp-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/implementation/ + - 패턴: <입력ID>_*.implementation.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Design 검증 규칙: + - Implementation 문서의 `입력 Design:` 줄에서 Design 문서 경로(1개 이상)를 확인한다. + - 쉼표(,)로 구분된 다중 경로를 허용한다. + - 연결 Design 문서가 하나라도 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Design 문서 상태가 하나라도 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. Implementation 개요`에 입력 Design/목적/우선순위 작업이 존재한다. + - `# 2. 이번 구현 범위`에 In Scope/Out of Scope가 모두 존재한다. + - `# 3. 작업 단위 분해`에 1개 이상의 구현 항목(I-001 등)이 존재한다. + - `# 4. 파일 변경 계획`에 1개 이상의 대상 파일이 존재한다. + - `# 5. 테스트/검증 계획`에 작업 단위별 검증 방법이 1개 이상 정의되어 있다. + - `# 6. 영향도 및 의존성 점검`에 코드/문서/설정(또는 동등 항목) 영향이 기록되어 있다. + - `# 7. Verification 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Verification 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 섹션 헤더(`##`)와 항목은 모두 제거한다. +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Implementation 파일 검색 +3. 문서 본문 읽기 +4. 연결 Design 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/implementation-draft.prompt.md b/.github/prompts/implementation-draft.prompt.md new file mode 100644 index 0000000..a04be83 --- /dev/null +++ b/.github/prompts/implementation-draft.prompt.md @@ -0,0 +1,85 @@ +--- +description: "Use when: /implementation-draft to generate implementation work breakdown, file change plan, and verification-ready execution steps from a Design document ID" +name: "implementation-draft" +argument-hint: "Enter a Design ID in dsn-001 format." +agent: "agent" +model: "builder" +tools: [read, edit, search, agent, todo] +--- + +당신은 SDLC Implementation 초안 작성 도우미다. +사용자가 입력한 Design 문서 ID를 기준으로 Implementation 초안(구현 작업 단위 분해 + 파일 변경 계획 + 테스트/검증 계획)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `dsn-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Design 파일 검색 규칙: + - 경로: docs/sdlc/design/ + - 패턴: <입력ID>_*.design.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Design 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Design이 아직 승인되지 않았다. /design-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Implementation 파일 생성 규칙: + - 저장 폴더: docs/sdlc/implementation/ + - 파일명: imp-<3자리문서번호>_YYYY-MM-DD_.implementation.md + - 문서번호: 기존 imp 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Implementation 본문 기본 템플릿: .github/templates/implementation-note.template.md +6. Implementation 작성 원칙: + - Design의 우선순위 상위 작업을 먼저 구현 단위로 분해한다. + - 각 작업은 작고 검증 가능한 단위(I-001, I-002 등)로 쪼갠다. + - 각 작업 단위마다 연결 설계/AC, 대상 파일, 변경 유형, 검증 방법을 기록한다. + - 코드/문서/설정 영향도와 의존성 점검 결과를 포함한다. + - Verification 단계에서 바로 사용할 수 있는 검증 근거를 준비하는 방향으로 작성한다. +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Implementation의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +9. Verification 전달 체크 규칙: + - 구현 범위가 Design과 추적 가능하게 연결되었는지 확인한다. + - 작업 단위별 검증 방법이 정의되었는지 확인한다. + - 영향도/의존성 점검이 반영되었는지 확인한다. +10. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Design 파일 검색 및 상태 확인 +3. Design 본문(우선순위 작업, 인터페이스, 테스트 전략, 리스크 대응) 읽기 +4. 구현 작업 단위와 파일 변경 계획 수립 +5. 테스트/검증 계획 및 영향도 점검 작성 +6. Implementation 문서 저장 +7. 본문 플레이스홀더 집계 후 # 9 작성 +8. 최종 응답에 처리 요약 제공 + +Implementation 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Implementation 개요 +2. 이번 구현 범위 +3. 작업 단위 분해 +4. 파일 변경 계획 +5. 테스트/검증 계획 +6. 영향도 및 의존성 점검 +7. Verification 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Design 문서 경로 +3. 형식 오류/미발견/미승인 여부 +4. Implementation 문서 생성/갱신 결과 +5. 추가된 사용자 결정 항목 요약 +6. 업데이트된 파일 경로 목록 +7. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/implementation.prompt.md b/.github/prompts/implementation.prompt.md new file mode 100644 index 0000000..25062bb --- /dev/null +++ b/.github/prompts/implementation.prompt.md @@ -0,0 +1,75 @@ +--- +description: "Use when: /implementation to execute specified work items from an imp document, perform real file operations, and update the imp doc status" +name: "implementation" +argument-hint: "Enter imp ID and item list. Examples: 'imp-001 I-004, I-005' or 'imp-001 P-002'" +agent: "agent" +model: "builder" +tools: [read, edit, search, create_file, agent, todo] +--- + +당신은 SDLC Implementation 실행 도우미다. +사용자가 지정한 imp 문서의 특정 작업 항목(I-ID 또는 P-ID)을 실제로 수행하고, 완료 후 imp 문서를 갱신한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 형식: `imp-<3자리숫자> <항목목록>` + - imp ID: `imp-<3자리숫자>` 형식 필수 + - 항목 목록: `I-001, I-002` 또는 `P-001, P-002` 또는 혼합 가능 + - 형식 오류 시 오류로 보고하고 종료한다. +3. imp 파일 검색 규칙: + - 경로: docs/sdlc/implementation/ + - 패턴: _*.implementation.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. imp 문서 상태 확인: + - 상태가 `draft`가 아니면 "이미 확정된 Implementation 문서다. 상태를 확인하라"로 보고하고 종료한다. +5. 항목 해석 규칙: + - I-ID 입력 시: imp 문서 `# 3. 작업 단위 분해` 표에서 해당 I-ID 행을 직접 찾는다. + - P-ID 입력 시: `# 3. 작업 단위 분해` 표에서 `연결 설계/AC` 열에 해당 P-ID가 포함된 모든 I-ID를 찾는다. + - 상태가 이미 `완료`인 항목은 건너뛰고 보고한다. + - 지정 항목이 실제 존재하지 않으면 오류로 보고하고 해당 항목만 건너뛴다. +6. 실행 전 Design 문서 참조: + - imp 문서 `# 1. Implementation 개요`의 `입력 Design` 경로로 Design 문서를 읽는다. + - 각 작업의 구체적 산출물 내용은 Design 문서의 `# 3. 인터페이스 정의`, `# 4. 데이터/흐름 설계`, `# 5. 테스트 전략 연계` 섹션을 근거로 한다. +7. 실행 원칙: + - `# 4. 파일 변경 계획` 표에서 해당 I-ID의 대상 파일과 변경 목적을 확인한다. + - 변경 종류가 `생성`이면 파일이 없을 때만 생성한다. 이미 존재하면 덮어쓰기 전 확인 보고 후 진행한다. + - 변경 종류가 `수정` 또는 `점검/수정`이면 파일을 읽은 후 필요한 부분만 수정한다. + - 하나의 I-ID에 여러 대상 파일이 있으면 모두 처리한다. + - 실행 중 오류 발생 시 해당 I-ID를 실패 처리하고 나머지 항목은 계속 진행한다. +8. imp 문서 갱신 규칙 (실행 완료 후): + - `# 3. 작업 단위 분해` 표: 완료된 항목의 `상태` 칼럼을 `완료`로 변경한다. + - `# 4. 파일 변경 계획` 표: 완료된 I-ID에 해당하는 행의 `상태` 칼럼을 `완료`로 변경한다. + - `# 8. 파일 처리 결과`: 이번 실행에서 생성/수정한 파일 경로를 추가한다. + - `# 9. 사용자 결정 필요 항목 요약`: 본문의 미해결 {{DECIDE}}/{{CONFIRM}}/{{DATA}} 재집계 후 갱신한다. +9. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} +10. 범위 제한: + - 이번 명령에서 지정하지 않은 I-ID는 절대 수정하지 않는다. + - imp 문서 외 SDLC 문서(Design, Planning 등)는 참조만 하고 수정하지 않는다. + - 지정한 파일 변경 계획 외의 추가 파일을 임의로 수정하지 않는다. + +실행 절차: +1. 입력 파싱: imp ID + 항목 목록 형식 검증 +2. imp 파일 검색 및 상태 확인 +3. 항목 해석: I-ID 직접 조회 또는 P-ID → I-ID 변환 +4. 상태가 이미 `완료`인 항목 필터링 및 보고 +5. Design 문서 읽기 (실행 내용 근거 확보) +6. 항목별 순서대로 실행: + a. `# 4. 파일 변경 계획`에서 대상 파일 목록 확인 + b. 대상 파일별 생성/수정 실행 + c. 실행 성공/실패 기록 +7. imp 문서 갱신 (`# 3`, `# 4`, `# 8`, `# 9`) +8. 최종 응답 제공 + +최종 응답 형식: +1. 실행 입력 요약 (imp ID, 지정 항목 목록) +2. 항목별 실행 결과 (성공/건너뜀/실패) +3. 생성/수정된 실제 파일 경로 목록 +4. imp 문서 갱신 내역 (상태 변경된 항목) +5. 잔여 미착수 항목 목록 +6. 다음 액션 제안 + +사용자 입력: +{{input}} diff --git a/.github/prompts/operation-confirm.prompt.md b/.github/prompts/operation-confirm.prompt.md new file mode 100644 index 0000000..13ebeac --- /dev/null +++ b/.github/prompts/operation-confirm.prompt.md @@ -0,0 +1,67 @@ +--- +description: "Use when: /operation-confirm to validate operations document readiness and mark it as confirmed from a Release ID" +name: "operation-confirm" +argument-hint: "Enter a Release ID in rel-001 format." +agent: "agent" +model: "operator" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Operations 승인 처리 도우미다. +사용자가 입력한 Release 문서 ID를 기준으로 연결된 Operations 문서를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `rel-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Operations 파일 탐색 규칙: + - 1차: rel-001 -> docs/sdlc/operations/ops-001_*.operations.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/operations/*.operations.md에서 `입력 Release:` 줄에 해당 rel ID 또는 경로가 포함된 문서 검색 + - 최종 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Release 검증 규칙: + - Operations 문서의 `입력 Release:` 줄에서 연결 Release 문서 경로를 확인한다. + - 연결 Release 문서가 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Release 문서 상태가 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. 배포 실행 기록`에 배포 방식/환경/사전검증/실행결과/검증결과가 모두 기록되었다. + - `# 2. 모니터링 관찰 결과`에 Release 정의 항목별 관찰 결과가 모두 기록되었다. + - `# 3. 인시던트 대응 기록`이 기록되었거나 "해당없음"으로 표시되었다. + - `# 4. Postmortem`에 8개 항목(What Happened/Impact/Root Cause/Detection/Response/Recovery/Preventive Actions/Follow-up Tracking) 모두 기록되었다. + - `# 5. 다음 반복 환류 항목`이 Discovery/Planning 입력으로 연결될 아이템과 담당자/일정을 포함한다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 6. 파일 처리 결과` 항목에 Operations 완료 시각과 마감 상태를 추가한다. + d. `# 7. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 `##` 하위 항목은 모두 제거한다. +8. 환류 항목 처리 규칙: + - `# 5. 다음 반복 환류 항목`에 명시된 각 개선 항목에 대해 다음 Discovery/Planning 백로그 연결을 확인한다. + - 자동으로 연결하지 않으며, 사용자가 수동 등록한 것만 인계 완료로 표시한다. + - 미연결 환류 항목이 있으면 "환류 항목 미연결" 항목에 추가해 보고한다. +9. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Operations 파일 탐색 +3. 문서 본문 읽기 +4. 연결 Release 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 환류 항목 미연결 여부 별도 보고 +9. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 완료 마킹, 플레이스홀더 정리) +6. 환류 항목 연결 상태 (연결됨/미연결 목록) +7. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/operation-draft.prompt.md b/.github/prompts/operation-draft.prompt.md new file mode 100644 index 0000000..4df4543 --- /dev/null +++ b/.github/prompts/operation-draft.prompt.md @@ -0,0 +1,85 @@ +--- +description: "Use when: /operation-draft to generate operations draft with deployment tracking, monitoring plan, and postmortem template from a Release document ID" +name: "operation-draft" +argument-hint: "Enter a Release ID in rel-001 format." +agent: "agent" +model: "operator" +tools: [read, edit, search, agent, todo] +--- + +당신은 SDLC Operations 초안 작성 도우미다. +사용자가 입력한 Release 문서 ID를 기준으로 Operations 초안(배포 실행 기록 + 모니터링 관찰 + Postmortem 템플릿)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `rel-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Release 파일 검색 규칙: + - 경로: docs/sdlc/release/ + - 패턴: <입력ID>_*.release.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Release 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Release가 아직 승인되지 않았다. /release-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Operations 파일 생성 규칙: + - 저장 폴더: docs/sdlc/operations/ + - 파일명: ops-<3자리문서번호>_YYYY-MM-DD_.operations.md + - 문서번호: 기존 ops 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - 본문 템플릿: .github/templates/operations-note.template.md + - append 금지, 생성/갱신 시 overwrite +6. Operations 작성 원칙: + - Release의 배포 체크리스트, 롤백 계획, 관찰 계획을 근거로 운영 추적 문맥을 작성한다. + - 배포 실행 기록(#1)은 배포 방식/환경/선정리/실행 결과/검증 결과를 포함한다. + - 모니터링 관찰(#2)은 Release #5 모니터링 항목별 관찰 결과를 기록한다. + - 인시던트 대응(#3)은 발생 인시던트/타임라인/영향도/우회책/복구 절차를 기록한다(없으면 해당없음). + - Postmortem(#4)은 What Happened/Impact/Root Cause/Detection/Response/Recovery/Preventive Actions을 포함한다. + - 환류 항목(#5)은 운영 학습을 다음 반복(Discovery/Planning)으로 연결할 아이템을 정의한다. +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - Operations 초안 생성 시점에 {{DATA:}} 플레이스홀더를 다수 사용하여 배포 실행자가 정보를 채우도록 한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자: Release 문서에서 인계받음 + - 승인 시각: `{{CONFIRM: 배포 완료 후 Operations 확인}}` +9. # 7 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Release 파일 검색 및 상태 확인 +3. Release 본문(배포 체크리스트, 롤백 계획, 관찰 계획, 릴리즈 노트) 읽기 +4. Release #1 입력 Verification, 배포 방식, 배포 환경 추출 +5. 배포 실행 기록 섹션 작성 ({{DATA:}} 플레이스홀더로 실행자 입력 대기) +6. 모니터링 관찰 섹션 생성 (Release #5 항목 복사, 관찰 결과는 {{DATA:}}) +7. 인시던트 대응 섹션 생성 (없으면 해당없음, Postmortem 템플릿 제공) +8. Postmortem 섹션 생성 (8개 항목 모두 {{DATA:}} 또는 등급별 예시 포함) +9. 환류 항목 섹션 생성 (발견된 개선 사항 추출 또는 {{DATA:}}) +10. Operations 문서 저장 +11. 본문 플레이스홀더 집계 후 # 7 작성 +12. 최종 응답에 처리 요약 제공 + +Operations 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. 배포 실행 기록 +2. 모니터링 관찰 결과 +3. 인시던트 대응 기록 +4. Postmortem +5. 다음 반복 환류 항목 +6. 파일 처리 결과 +7. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Release 문서 경로 +3. Operations 문서 생성/갱신 결과 +4. 배포 실행 기록 초안 요약 +5. 모니터링 관찰 항목 복사 상세 +6. Postmortem 템플릿 완성도 +7. 업데이트된 파일 경로 목록 +8. 다음 액션 (배포 실행자가 DATA 항목 채우기) + +사용자 입력: +{{input}} diff --git a/.github/prompts/operation-review.prompt.md b/.github/prompts/operation-review.prompt.md new file mode 100644 index 0000000..1741fd5 --- /dev/null +++ b/.github/prompts/operation-review.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Use when: /operation-review to validate operations document completeness and update records from a Release ID" +name: "operation-review" +argument-hint: "Enter a Release ID in rel-001 format." +agent: "agent" +model: "operator" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Operations 재검토 도우미다. +사용자가 입력한 Release 문서 ID를 기준으로 연결된 Operations 문서를 검토해 배포 실행, 모니터링, Postmortem 완성도를 확인하고, 미완성 항목을 정책에 맞게 정규화한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `rel-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Operations 파일 탐색 규칙: + - 1차: rel-001 -> docs/sdlc/operations/ops-001_*.operations.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/operations/*.operations.md에서 `입력 Release:` 줄에 해당 rel ID 또는 경로가 포함된 문서 검색 + - 최종 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Release 검증 규칙: + - Operations 문서의 `입력 Release:` 줄에서 연결 Release 문서 경로를 확인한다. + - 연결 Release 문서가 존재하지 않으면 검토 불가로 보고하고 종료한다. + - 연결 Release 문서 상태가 `confirmed`가 아니면 검토 불가로 보고하고 종료한다. +5. 검토 항목(모두 충족 여부 점검, 불만족해도 진행): + - `# 1. 배포 실행 기록`: 배포 방식/환경/사전검증/실행결과/검증결과가 모두 기록되었는가? + - `# 2. 모니터링 관찰 결과`: Release #5 정의된 모니터링 항목별 관찰 결과가 모두 기록되었는가? + - `# 3. 인시던트 대응 기록`: 발생한 인시던트가 있으면 타임라인/영향도/우회책/복구 절차가 기록되었는가? (없으면 해당없음으로 표시) + - `# 4. Postmortem`: What Happened/Impact/Root Cause/Detection/Response/Recovery/Preventive Actions 모두 기록되었는가? + - `# 5. 다음 반복 환류 항목`: 운영 학습을 기반한 개선 항목과 담당자/일정이 명시되었는가? + - 본문에 `{{DATA:` 플레이스홀더가 남아 있으면 미완성으로 표시한다. +6. 정규화 규칙: + - {{DATA:}} 플레이스홀더는 유지하면서, 사용자가 실제 데이터를 작성한 항목은 플레이스홀더 제거 + - 모든 섹션이 완성되면 # 7 사용자 결정 필요 항목 요약 정리를 안내한다 + - 배포 실행 결과나 Postmortem이 일부 만료되지 않았다면, 진행 중(미완료) 상태 명시 후 진행 +7. 응답 규칙: + - 검토 결과를 미충족 항목 목록으로 보고한다 (별도 수정 없음) + - 사용자가 문서를 갱신한 후 다시 실행하도록 안내한다 +8. 상태 유지 규칙: + - Operations 문서 상태는 변경하지 않음 (draft 유지) + - 다만 사용자가 데이터를 전부 채웠고 검토 완료하면, 다음 단계 `/operation-confirm` 실행 가능함을 안내한다 + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Operations 파일 탐색 +3. 문서 본문 읽기 +4. 연결 Release 문서 존재/상태 확인 +5. 배포 실행/모니터링/인시던트/Postmortem/환류 5개 섹션 각각 검토 +6. 각 섹션별 미완성 항목 확인 +7. {{DATA:}} 플레이스홀더 목록 추출 +8. 검토 결과 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 검토 결과 (완료/미완료) +4. 미완성 항목 목록: + - 섹션별 미충족 항목 + - 남은 {{DATA:}} 플레이스홀더 목록 +5. 완성도 진척율 (예: 배포 실행 기록 80%, 모니터링 관찰 100% ...) +6. 다음 액션 (미완성 항목 보충 또는 /operation-confirm 실행) + +사용자 입력: +{{input}} diff --git a/.github/prompts/planning-confirm.prompt.md b/.github/prompts/planning-confirm.prompt.md new file mode 100644 index 0000000..cc875ae --- /dev/null +++ b/.github/prompts/planning-confirm.prompt.md @@ -0,0 +1,56 @@ +--- +description: "Use when: /planning-confirm to validate planning document readiness and mark it as confirmed" +name: "planning-confirm" +argument-hint: "Enter a Planning ID in pln-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Planning 승인 처리 도우미다. +사용자가 입력한 Planning 문서 ID를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `pln-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/planning/ + - 패턴: <입력ID>_*.planning.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 승인 가능 여부 판断 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 2. 이번 반복 범위 경계`에 In Scope/Out of Scope가 모두 존재한다. + - `# 3. 백로그 작업 분해`에 1개 이상의 작업 항목이 존재한다. + - `# 4. WSJF 점수표`에 1개 이상의 항목과 Priority가 존재한다. + - `# 5. 작업별 수용 기준 (AC)`에 각 작업별 AC가 최소 2개 이상 존재한다. + - `# 7. Design 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +5. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +6. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Design 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 섹션 헤더(`##`)와 항목은 모두 제거한다. +7. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Planning 파일 검색 +3. 문서 본문 읽기 +4. 승인 가능 여부 체크리스트 전체 점검 +5. 승인 불가 항목이 있으면 보고하고 종료 +6. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +7. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/planning-draft.prompt.md b/.github/prompts/planning-draft.prompt.md new file mode 100644 index 0000000..2602edf --- /dev/null +++ b/.github/prompts/planning-draft.prompt.md @@ -0,0 +1,95 @@ +--- +description: "Use when: /planning-draft to generate planning backlog, WSJF scoring table, and acceptance criteria from a Discovery document ID" +name: "planning-draft" +argument-hint: "Enter a Discovery ID in dcy-001 format." +agent: "agent" +model: "planner" +tools: [read, edit, search, web, agent, todo] +--- + +당신은 SDLC Planning 초안 작성 도우미다. +사용자가 입력한 Discovery 문서 ID를 기준으로 Planning 초안(백로그 + WSJF 표 + AC)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `dcy-<3자리숫자>` 형식만 허용한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화 + - 중복 입력 제거 +4. Discovery 파일 검색 규칙: + - 경로: docs/sdlc/discovery/ + - 패턴: <입력ID>_*.discovery.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +5. Planning 파일 생성 규칙: + - 저장 폴더: docs/sdlc/planning/ + - 파일명: pln-<3자리문서번호>_YYYY-MM-DD_.planning.md + - 문서번호: 기존 pln 번호 최대값 + 1 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Planning 본문 기본 템플릿: .github/templates/planning-note.template.md +6. 점수 산출물 규칙: + - 점수 CSV 결과 파일 경로: docs/sdlc/planning/planning-backlog-scores.out.csv + - 템플릿 파일 경로: .github/templates/planning-backlog-scores.template.csv + - 생성/갱신된 Planning 문서와 점수 CSV의 작업 항목/우선순위가 일치해야 한다. +7. Planning 작성 원칙: + - Discovery의 FR/NFR/리스크/성공기준을 작업 단위로 분해한다. + - 기본 우선순위 방식은 WSJF를 사용한다. + - WSJF 계산 근거(BV/TC/RR-OE/Job Size)는 항목별로 설명한다. + - 작업마다 수용 기준(AC)을 최소 2개 이상 정의한다. +8. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +9. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Planning의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +10. 범위/우선순위 규칙: + - In Scope/Out of Scope를 명시한다. + - WSJF 동률 시 RR/OE가 큰 항목을 우선한다. + - 동률이 계속되면 Job Size가 작은 항목을 우선한다. +11. DoD 체크 규칙: + - Design 단계 전달 가능 여부를 문서 내 체크리스트로 표시한다. + - 상위 우선순위 항목의 AC 검증 가능성을 확인한다. +12. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 본문에 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Discovery 파일 검색 및 본문 읽기 +3. FR/NFR/리스크/성공지표를 작업 항목으로 분해 +4. WSJF 점수 계산 및 우선순위 정렬 +5. 작업별 AC 작성 +6. Planning 문서 저장 +7. 점수 CSV 저장(또는 갱신) +8. 본문 플레이스홀더 집계 후 # 9 작성 +9. 최종 응답에 처리 요약 제공 + +Planning 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Planning 개요 +2. 이번 반복 범위 경계 +3. 백로그 작업 분해 +4. WSJF 점수표 +5. 작업별 수용 기준 (AC) +6. 우선순위 조정/예외 +7. Design 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 Discovery ID +3. 형식 오류/미발견 여부 +4. Planning 문서 생성/갱신 결과 +5. 점수 CSV 생성/갱신 결과 +6. 추가된 사용자 결정 항목 요약 +7. 업데이트된 파일 경로 목록 +8. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/planning-review.prompt.md b/.github/prompts/planning-review.prompt.md new file mode 100644 index 0000000..f01fa02 --- /dev/null +++ b/.github/prompts/planning-review.prompt.md @@ -0,0 +1,79 @@ +--- +description: "Use when: /planning-review to re-review existing planning documents and update them in place from Discovery IDs" +name: "planning-review" +argument-hint: "Enter Discovery IDs separated by commas. Example: dcy-001, dcy-002" +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Planning 문서 재검토 도우미다. +사용자가 지정한 Discovery 문서 ID 목록을 기준으로 Planning 문서를 재검토하고, 필요한 보정을 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 Discovery ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(dcy-001 형식) + - 중복 ID 제거 +4. ID 형식은 dcy-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. Planning 파일 검색 규칙: + - 1차: dcy-001 -> docs/sdlc/planning/pln-001_*.planning.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/planning/*.planning.md에서 `입력 Discovery:` 줄에 dcy-001 포함 문서 검색 +6. 존재하지 않는 Planning 문서는 생성하지 않는다. + - 미발견 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. +7. 점수 산출물 정책: + - 산출물 경로는 docs/sdlc/planning/planning-backlog-scores.out.csv를 기본으로 사용한다. + - .github는 템플릿/스크립트 자산 위치이며, out.csv 생성물 경로로 사용하지 않는다. +8. 문서 재검토 항목: + - 섹션 0~9 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드가 모두 존재하는지 검증 + - 필드 누락 시 플레이스홀더로 보정(상태 불명확 -> `draft`, 승인 시각 미기재 -> `{{CONFIRM: 승인 후 기재}}`) + - `# 2. 이번 반복 범위 경계`에 In Scope/Out of Scope가 모두 존재하는지 검증 + - `# 3. 백로그 작업 분해`에 Item ID, Work Item, 연결 요구사항, 연결 리스크, 담당 컬럼이 존재하는지 검증 + - `# 4. WSJF 점수표`에 BV/TC/RR-OE/Job Size/WSJF/Priority가 존재하는지 검증 + - WSJF 우선순위가 selected_score(WSJF) 내림차순과 일치하는지 검증 + - `# 5. 작업별 수용 기준 (AC)`에서 각 작업별 AC가 최소 2개 이상인지 검증 + - `# 7. Design 단계 전달 체크`의 체크 근거가 본문과 일치하는지 검증 + - `# 8. 파일 처리 결과`의 점수 파일 경로가 docs/sdlc/planning/planning-backlog-scores.out.csv인지 검증 +9. CSV 정합성 검증: + - docs/sdlc/planning/planning-backlog-scores.out.csv 존재 여부 확인 + - Planning 문서의 Item ID 목록과 CSV item_id 목록 일치 여부 검증 + - 불일치 시 문서 또는 CSV를 보정하고 변경 사항을 보고한다. +10. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/내용은 유지하되 누락/불일치/중복만 보정 +11. 플레이스홀더 규칙: + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - 추가 의사결정이 필요한 경우 반드시 플레이스홀더를 삽입한다. +12. 플레이스홀더 집계 규칙: + - 본문에 미해결 플레이스홀더가 있으면 `# 9. 사용자 결정 필요 항목 요약`에 유형별로 집계한다. + - 본문에 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. +13. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. + +실행 절차: +1. 입력 ID 목록 정규화 및 형식 검증 +2. 각 ID에 대해 Planning 문서 탐색(1차/2차 규칙 적용) +3. 발견 문서 내용 읽기 +4. 재검토 항목 기준으로 누락/불일치 판단 +5. 필요한 항목만 보정하여 문서 overwrite 저장 +6. 점수 CSV와 Item ID/우선순위 정합성 검증 및 필요 시 보정 +7. 문서별 플레이스홀더를 #9에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 Discovery ID 목록 +3. 형식 오류 ID 목록(있으면) +4. 미발견 ID 목록(있으면) +5. 문서별 검토 결과 +6. 문서별 수정 내역 요약 +7. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +8. 업데이트된 파일 경로 목록 +9. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/release-confirm.prompt.md b/.github/prompts/release-confirm.prompt.md new file mode 100644 index 0000000..4a7b786 --- /dev/null +++ b/.github/prompts/release-confirm.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Use when: /release-confirm to validate release document readiness and mark it as confirmed from a Verification ID" +name: "release-confirm" +argument-hint: "Enter a Verification ID in ver-001 format." +agent: "agent" +model: "releaser" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Release 승인 처리 도우미다. +사용자가 입력한 Verification 문서 ID를 기준으로 연결된 Release 문서를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `ver-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Release 파일 탐색 규칙: + - 1차: ver-001 -> docs/sdlc/release/rel-001_*.release.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/release/*.release.md에서 `입력 Verification:` 줄에 해당 ver ID 또는 경로가 포함된 문서 검색 + - 최종 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Verification 검증 규칙: + - Release 문서의 `입력 Verification:` 줄에서 연결 Verification 문서 경로를 확인한다. + - 연결 Verification 문서가 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Verification 문서 상태가 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. Release 개요`에 입력 Verification/배포 대상 범위/배포 방식/배포 환경이 존재한다. + - `# 2. 배포 체크리스트`의 필수 6개 항목 결과가 모두 `완료` 또는 `해당없음`이다. + - `# 3. 롤백 계획`에 롤백 트리거/절차/데이터 롤백 전략/롤백 후 검증/롤백 커뮤니케이션이 모두 존재한다. + - `# 4. 릴리즈 노트 초안`에 변경 요약/주요 변경 내용/영향 범위/알려진 제한 사항이 존재한다. + - `# 5. 배포 후 관찰 계획`에 모니터링 항목 1개 이상과 관찰 기간이 존재한다. + - `# 6. 승인된 예외 인계 목록`이 존재한다(예외 없음은 "해당 없음" 허용). + - `# 7. Operations 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Operations 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 `##` 하위 항목은 모두 제거한다. +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Release 파일 탐색 +3. 문서 본문 읽기 +4. 연결 Verification 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/release-draft.prompt.md b/.github/prompts/release-draft.prompt.md new file mode 100644 index 0000000..9dd81b5 --- /dev/null +++ b/.github/prompts/release-draft.prompt.md @@ -0,0 +1,85 @@ +--- +description: "Use when: /release-draft to generate release draft, rollback plan, and operations handoff from a Verification document ID" +name: "release-draft" +argument-hint: "Enter a Verification ID in ver-001 format." +agent: "agent" +model: "releaser" +tools: [read, edit, search, agent, todo] +--- + +당신은 SDLC Release 초안 작성 도우미다. +사용자가 입력한 Verification 문서 ID를 기준으로 Release 초안(배포 체크리스트 + 롤백 계획 + 릴리즈 노트 + 관찰 계획)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `ver-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Verification 파일 검색 규칙: + - 경로: docs/sdlc/verification/ + - 패턴: <입력ID>_*.verification.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Verification 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Verification이 아직 승인되지 않았다. /verification-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Release 파일 생성 규칙: + - 저장 폴더: docs/sdlc/release/ + - 파일명: rel-<3자리문서번호>_YYYY-MM-DD_.release.md + - 문서번호: 기존 rel 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Release 본문 기본 템플릿: .github/templates/release-note.template.md +6. Release 작성 원칙: + - Verification의 품질 게이트 판정, 결함/예외, 회귀 결과를 근거로 배포 가능성 문맥을 작성한다. + - 배포 체크리스트(#2)는 항목별 결과(완료/미완료/해당없음)와 근거를 남긴다. + - 롤백 계획(#3)은 트리거/절차/데이터 롤백/검증/커뮤니케이션을 모두 포함한다. + - 릴리즈 노트(#4)는 변경 요약/영향 범위/알려진 제한 사항을 포함한다. + - 관찰 계획(#5)은 모니터링 지표/임계치/담당자를 포함한다. + - 승인된 예외 인계(#6)는 Verification 예외와 연결해 기록한다(없으면 해당 없음). +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Release의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +9. Operations 전달 체크 규칙: + - 롤백 가능한 배포 계획과 배포 체크리스트 근거가 있어야 한다. + - 관찰 포인트/장애 트리거/후속 대응 절차가 정의되어야 한다. + - 승인된 예외가 있으면 인계 이력이 포함되어야 한다. +10. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Verification 파일 검색 및 상태 확인 +3. Verification 본문(품질 게이트, 결함/예외, 회귀 결과, Release 전달 체크) 읽기 +4. 배포 체크리스트/롤백 계획/릴리즈 노트/관찰 계획 작성 +5. Release 문서 저장 +6. 본문 플레이스홀더 집계 후 # 9 작성 +7. 최종 응답에 처리 요약 제공 + +Release 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Release 개요 +2. 배포 체크리스트 +3. 롤백 계획 +4. 릴리즈 노트 초안 +5. 배포 후 관찰 계획 +6. 승인된 예외 인계 목록 +7. Operations 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Verification 문서 경로 +3. 형식 오류/미발견/미승인 여부 +4. Release 문서 생성/갱신 결과 +5. 추가된 사용자 결정 항목 요약 +6. 업데이트된 파일 경로 목록 +7. 다음 액션 + +사용자 입력: +{{input}} \ No newline at end of file diff --git a/.github/prompts/release-review.prompt.md b/.github/prompts/release-review.prompt.md new file mode 100644 index 0000000..9bab238 --- /dev/null +++ b/.github/prompts/release-review.prompt.md @@ -0,0 +1,80 @@ +--- +description: "Use when: /release-review to re-review existing release documents and update them in place from Verification IDs" +name: "release-review" +argument-hint: "Enter Verification IDs separated by commas. Example: ver-001, ver-002" +agent: "agent" +model: "releaser" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Release 문서 재검토 도우미다. +사용자가 지정한 Verification 문서 ID 목록을 기준으로 해당 Release 문서를 재검토하고, 필요한 보정을 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 Verification ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(ver-001 형식) + - 중복 ID 제거 +4. ID 형식은 `ver-<3자리숫자>`만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. Release 파일 탐색 규칙: + - 1차: ver-001 -> docs/sdlc/release/rel-001_*.release.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/release/*.release.md에서 `입력 Verification:` 줄에 해당 ver ID 또는 경로가 포함된 문서 검색 +6. 존재하지 않는 Release 문서는 생성하지 않는다. + - 미발견 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. +7. 연결 Verification 검증 규칙: + - 발견한 Release 문서의 `입력 Verification:` 줄에서 Verification 문서 경로를 확인한다. + - 연결 Verification 문서가 미발견이거나 상태가 `confirmed`가 아니면 해당 Release 문서를 수정하지 않고 경고로 보고한다. +8. 문서 재검토 항목: + - 섹션 0~9 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드 존재 여부 검증 + - 필드 누락 시 플레이스홀더로 보정(상태 불명확 -> `draft`, 승인 시각 미기재 -> `{{CONFIRM: 승인 후 기재}}`) + - `# 1. Release 개요`에 입력 Verification/배포 대상 범위/배포 방식/배포 환경 존재 여부 검증 + - `# 2. 배포 체크리스트`에 6개 필수 항목(설정/데이터/호환성/의존성/트래픽/커뮤니케이션) 존재 여부 검증 + - `# 3. 롤백 계획`에 트리거/절차/데이터 롤백/검증/커뮤니케이션/참조 런북 존재 여부 검증 + - `# 4. 릴리즈 노트 초안`에 변경 요약/주요 변경/영향 범위/제한 사항 존재 여부 검증 + - `# 5. 배포 후 관찰 계획`에 모니터링 표 1개 이상과 관찰 기간 존재 여부 검증 + - `# 6. 승인된 예외 인계 목록` 존재 여부 검증(예외 없음은 `해당 없음` 허용) + - `# 7. Operations 단계 전달 체크`의 체크 항목 수/문구/근거 일치성 검증 + - `# 8. 파일 처리 결과` 경로가 현재 문서 경로와 일치하는지 검증 +9. 체크 정규화 규칙: + - `# 7. Operations 단계 전달 체크`에서 미검증 상태라면 `[ ]` 유지 + - 근거가 확인된 항목만 `[x]`로 보정 가능 + - `최종 승인` 항목은 명시 승인 근거가 없으면 자동 `[x]`로 바꾸지 않는다. +10. 플레이스홀더 규칙: + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} +11. 플레이스홀더 집계 규칙: + - 본문의 미해결 플레이스홀더를 `# 9. 사용자 결정 필요 항목 요약`에 유형별 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. +12. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/근거를 유지하면서 누락/불일치/중복만 보정 +13. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. + +실행 절차: +1. 입력 ID 목록 정규화 및 형식 검증 +2. 각 ID에 대해 Release 문서 탐색(1차/2차 규칙 적용) +3. 발견 문서의 연결 Verification 존재/상태 확인 +4. 발견 문서 내용 읽기 +5. 재검토 항목 기준으로 누락/불일치 판단 +6. 필요한 항목만 보정하여 문서 overwrite 저장 +7. 문서별 플레이스홀더를 #9에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 Verification ID 목록 +3. 형식 오류 ID 목록(있으면) +4. Release 미발견 ID 목록(있으면) +5. 연결 Verification 미승인/미발견 목록(있으면) +6. 문서별 검토 결과 +7. 문서별 수정 내역 요약 +8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +9. 업데이트된 파일 경로 목록 +10. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/verification-confirm.prompt.md b/.github/prompts/verification-confirm.prompt.md new file mode 100644 index 0000000..76aec10 --- /dev/null +++ b/.github/prompts/verification-confirm.prompt.md @@ -0,0 +1,63 @@ +--- +description: "Use when: /verification-confirm to validate verification document readiness and mark it as confirmed" +name: "verification-confirm" +argument-hint: "Enter a Verification ID in ver-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo] +--- + +당신은 SDLC Verification 승인 처리 도우미다. +사용자가 입력한 Verification 문서 ID를 검토해 승인 가능 여부를 판단하고, 승인 가능하면 문서를 confirmed 상태로 전환한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `ver-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. 파일 검색 규칙: + - 경로: docs/sdlc/verification/ + - 패턴: <입력ID>_*.verification.md + - 미발견 시 미발견으로 보고하고 종료한다. +4. 연결 Implementation 검증 규칙: + - Verification 문서의 `입력 Implementation:` 줄에서 연결된 Implementation 문서 경로를 확인한다. + - 연결 Implementation 문서가 존재하지 않으면 승인 불가로 보고하고 종료한다. + - 연결 Implementation 문서 상태가 `confirmed`가 아니면 승인 불가로 보고하고 종료한다. +5. 승인 가능 여부 판단 기준(모두 충족해야 승인 가능): + - `# 0. 문서 상태`의 상태가 `draft`인지 확인한다. 이미 `confirmed`이면 "이미 승인된 문서"로 보고하고 종료한다. + - `# 1. Verification 개요`에 입력 Implementation/목적/검증 대상 작업이 존재한다. + - `# 2. 검증 범위`에 In Scope/Out of Scope가 모두 존재한다. + - `# 3. AC별 검증 결과`에 1개 이상의 검증 항목이 존재한다. + - `# 4. 결함 목록`과 심각도 기준이 존재한다. + - `# 5. 회귀 테스트 결과`에 1개 이상의 테스트 항목이 존재한다. + - `# 6. 예외 승인 목록`이 존재한다(예외 없음은 "해당 없음" 허용). + - `# 7. Release 단계 전달 체크`에서 최종 승인 이외의 체크 항목이 모두 `[x]`이다. + - 본문에 `{{DECIDE:` 또는 `{{DATA:` 형태의 미해결 플레이스홀더가 없다. +6. 승인 불가 조건에 해당하면 해당 항목을 목록으로 보고하고 종료한다. 문서를 수정하지 않는다. +7. 승인 가능 시 아래 순서로 문서를 수정한다: + a. `# 0. 문서 상태`의 상태를 `confirmed`로 변경한다. + b. `# 0. 문서 상태`의 승인 시각을 현재 날짜(YYYY-MM-DD)로 기재한다. `{{CONFIRM:` 형식이 남아 있으면 현재 날짜로 대체한다. + c. `# 7. Release 단계 전달 체크`의 최종 승인 항목을 `[x]`로 변경하고, 아래 `{{CONFIRM:` 줄을 제거한다. + d. `# 9. 사용자 결정 필요 항목 요약`의 내용을 `현재 미해결 플레이스홀더 없음`으로 대체한다. 기존 섹션 헤더(`##`)와 항목은 모두 제거한다. +8. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장한다. + - 위의 4개 항목(a~d) 외에는 기존 내용을 변경하지 않는다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Verification 파일 검색 +3. 문서 본문 읽기 +4. 연결 Implementation 문서 존재/상태 확인 +5. 승인 가능 여부 체크리스트 전체 점검 +6. 승인 불가 항목이 있으면 보고하고 종료 +7. 승인 가능하면 a~d 순서로 문서 수정 및 overwrite 저장 +8. 최종 응답에 처리 결과 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 대상 파일 경로 +3. 승인 가능 여부 (가능/불가) +4. 승인 불가 시: 미충족 항목 목록 +5. 승인 완료 시: 수정 내역 요약 (상태 변경, 승인 시각, 체크 항목, 플레이스홀더 정리) +6. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/verification-draft.prompt.md b/.github/prompts/verification-draft.prompt.md new file mode 100644 index 0000000..25eaea8 --- /dev/null +++ b/.github/prompts/verification-draft.prompt.md @@ -0,0 +1,84 @@ +--- +description: "Use when: /verification-draft to generate verification draft, quality-gate checks, and defect tracking plan from an Implementation document ID" +name: "verification-draft" +argument-hint: "Enter an Implementation ID in imp-001 format." +agent: "agent" +model: "reviewer" +tools: [read, edit, search, agent, todo] +--- + +당신은 SDLC Verification 초안 작성 도우미다. +사용자가 입력한 Implementation 문서 ID를 기준으로 Verification 초안(AC 검증 결과 구조 + 결함 분류 + 품질 게이트 판정 + Release 전달 준비)을 작성하고 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 ID는 `imp-<3자리숫자>` 형식만 허용한다. 형식 오류 시 오류로 보고하고 종료한다. +3. Implementation 파일 검색 규칙: + - 경로: docs/sdlc/implementation/ + - 패턴: <입력ID>_*.implementation.md + - 미발견 시 파일 생성 없이 미발견으로 보고하고 종료한다. +4. Implementation 문서 상태 확인: + - 상태가 `confirmed`가 아니면 "Implementation이 아직 승인되지 않았다. /implementation-confirm을 먼저 실행하라"로 보고하고 종료한다. +5. Verification 파일 생성 규칙: + - 저장 폴더: docs/sdlc/verification/ + - 파일명: ver-<3자리문서번호>_YYYY-MM-DD_.verification.md + - 문서번호: 기존 ver 번호 최대값 + 1, 없으면 001부터 시작 + - 날짜: 생성일 기준, 이후 수정 시 파일명 날짜 유지 + - append 금지, 생성/갱신 시 overwrite + - Verification 본문 기본 템플릿: .github/templates/verification-note.template.md +6. Verification 작성 원칙: + - Implementation의 작업 단위/검증 계획(#3, #5)을 기준으로 AC별 검증 결과 틀을 만든다. + - 결함 분류는 Blocker/Major/Minor 기준을 유지한다. + - 품질 게이트 판정(Blocker 0, Major 0 또는 승인 예외)을 명시한다. + - Release로 전달할 배포 가능 여부/예외 목록/잔여 리스크를 포함한다. +7. 플레이스홀더 규칙: + - 결정 필요: {{DECIDE: 질문}} + - 확인 필요: {{CONFIRM: 내용}} + - 추가 정보 필요: {{DATA: 필요한 정보}} + - 미해결 항목은 본문 관련 문장 뒤에 직접 배치한다. +8. 문서 상태 규칙: + - 최상단 `# 0. 문서 상태` 필수 + - 상태: 초안 생성 시 `draft` + - 승인자 불명확 시 `{{DECIDE: 이 Verification의 최종 승인자는 누구인가?}}` + - 승인 시각 미확정 시 `{{CONFIRM: 승인 후 기재}}` +9. Release 전달 체크 규칙: + - Blocker/Major 및 예외 승인 상태가 점검 가능해야 한다. + - 테스트 실행 근거가 문서에 연결되어야 한다. + - Release 전달 항목(배포 가능 여부, 잔여 리스크, 승인 예외)이 정리되어야 한다. +10. # 9 사용자 결정 필요 항목 요약 규칙: + - 본문의 {{DECIDE}}/{{CONFIRM}}/{{DATA}}를 유형별로 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 작성한다. + +실행 절차: +1. 입력 ID 정규화 및 형식 검증 +2. Implementation 파일 검색 및 상태 확인 +3. Implementation 본문(작업 단위, 검증 계획, 영향도) 읽기 +4. AC별 검증 결과/결함/회귀/예외 구조 작성 +5. Release 전달 체크 항목 작성 +6. Verification 문서 저장 +7. 본문 플레이스홀더 집계 후 # 9 작성 +8. 최종 응답에 처리 요약 제공 + +Verification 문서 출력 섹션 순서(고정): +0. 문서 상태 +1. Verification 개요 +2. 검증 범위 +3. AC별 검증 결과 +4. 결함 목록 +5. 회귀 테스트 결과 +6. 예외 승인 목록 +7. Release 단계 전달 체크 +8. 파일 처리 결과 +9. 사용자 결정 필요 항목 요약 + +최종 응답 형식: +1. 실행 입력 요약 +2. 입력 Implementation 문서 경로 +3. 형식 오류/미발견/미승인 여부 +4. Verification 문서 생성/갱신 결과 +5. 추가된 사용자 결정 항목 요약 +6. 업데이트된 파일 경로 목록 +7. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/verification-review.prompt.md b/.github/prompts/verification-review.prompt.md new file mode 100644 index 0000000..d7c8acd --- /dev/null +++ b/.github/prompts/verification-review.prompt.md @@ -0,0 +1,83 @@ +--- +description: "Use when: /verification-review to re-review existing verification documents and update them in place from Verification IDs" +name: "verification-review" +argument-hint: "Enter Verification IDs separated by commas. Example: ver-001, ver-002" +agent: "agent" +model: "reviewer" +tools: [read, edit, search, todo, agent] +--- + +당신은 SDLC Verification 문서 재검토 도우미다. +사용자가 지정한 Verification 문서 ID 목록을 기준으로 해당 Verification 문서를 재검토하고, 필요한 보정을 반영해 저장한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력은 쉼표(,) 기준으로 분리해 Verification ID 목록으로 처리한다. +3. 입력 정규화 규칙: + - 공백 제거 + - 소문자 정규화(ver-001 형식) + - 중복 ID 제거 +4. ID 형식은 `ver-<3자리숫자>`만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +5. Verification 파일 검색 규칙: + - 경로: `docs/sdlc/verification/` + - 패턴: `<입력ID>_*.verification.md` + - 미발견 ID는 미발견 목록으로 보고하고, 존재 문서만 재검토한다. +6. 존재하지 않는 Verification 문서는 생성하지 않는다. +7. 연결 Implementation 검증 규칙: + - 발견한 Verification 문서의 `입력 Implementation:` 줄에서 연결 경로를 확인한다. + - 연결된 Implementation 문서가 미발견이거나 상태가 `confirmed`가 아니면 해당 Verification 문서를 수정하지 않고 경고로 보고한다. +8. 문서 재검토 항목: + - 섹션 0~9 순서/유일성 검증 + - `# 0. 문서 상태`에 상태(draft/confirmed), 승인자, 승인 시각 3개 필드 존재 여부 검증 + - 상태가 누락/비정상 값이면 `draft`로 보정 + - 승인자 누락 시 `{{DECIDE: 이 Verification의 최종 승인자는 누구인가?}}`로 보정 + - 승인 시각 누락 시 `{{CONFIRM: 승인 후 기재}}`로 보정 + - `# 1. Verification 개요`에 입력 Implementation/목적/검증 대상 작업 존재 여부 검증 + - `# 2. 검증 범위`에 In Scope/Out of Scope 존재 여부 검증 + - `# 3. AC별 검증 결과`에 1개 이상의 검증 항목과 품질 게이트 판정 존재 여부 검증 + - `# 4. 결함 목록`과 심각도 기준 존재 여부 검증 + - `# 5. 회귀 테스트 결과`와 회귀 리스크 평가 존재 여부 검증 + - `# 6. 예외 승인 목록` 존재 여부 검증(예외 없음은 `해당 없음` 허용) + - `# 7. Release 단계 전달 체크`의 체크 항목 수/문구/근거 일치성 검증 + - `# 8. 파일 처리 결과`의 경로가 현재 문서 경로와 일치하는지 검증 +9. 체크 정규화 규칙: + - `# 7. Release 단계 전달 체크`에서 미검증 상태라면 `[ ]` 유지 + - 품질 게이트와 근거가 충족된 항목은 `[x]`로 보정 가능 + - `최종 승인` 항목은 명시 승인 근거가 없으면 자동 `[x]`로 바꾸지 않는다. +10. 플레이스홀더 규칙: + - {{DECIDE: 질문}} + - {{CONFIRM: 내용}} + - {{DATA: 필요한 정보}} + - 추가 의사결정/확인/데이터가 필요한 경우 본문 관련 위치에 삽입한다. +11. 플레이스홀더 집계 규칙: + - 본문의 미해결 플레이스홀더를 `# 9. 사용자 결정 필요 항목 요약`에 유형별 집계한다. + - 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. +12. 수정 규칙: + - append 금지, 단일 완성본으로 overwrite 저장 + - 기존 사용자 의도/근거를 유지하면서 누락/불일치/중복만 보정 +13. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. + +실행 절차: +1. 입력 ID 목록 정규화 및 형식 검증 +2. 각 ID에 대해 Verification 문서 탐색 +3. 발견 문서의 연결 Implementation 존재/상태 확인 +4. 발견 문서 내용 읽기 +5. 재검토 항목 기준으로 누락/불일치 판단 +6. 필요한 항목만 보정하여 문서 overwrite 저장 +7. 문서별 플레이스홀더를 #9에 집계/정규화 +8. 최종 응답에 처리 요약 제공 + +최종 응답 형식: +1. 실행 입력 요약 +2. 정규화된 대상 Verification ID 목록 +3. 형식 오류 ID 목록(있으면) +4. Verification 미발견 ID 목록(있으면) +5. 연결 Implementation 미승인/미발견 목록(있으면) +6. 문서별 검토 결과 +7. 문서별 수정 내역 요약 +8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +9. 업데이트된 파일 경로 목록 +10. 다음 액션 + +사용자 입력: +{{input}} \ No newline at end of file diff --git a/.github/scripts/bootstrap.sh b/.github/scripts/bootstrap.sh new file mode 100644 index 0000000..11c0ee0 --- /dev/null +++ b/.github/scripts/bootstrap.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" + +log() { + printf '[bootstrap] %s\n' "$*" +} + +fail() { + printf '[bootstrap] ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_linux() { + if [ "$(uname -s)" != "Linux" ]; then + fail "This bootstrap script supports Linux only." + fi +} + +ensure_repo_structure() { + local dirs=( + ".github/agents" + ".github/instructions" + ".github/prompts" + ".github/runbooks/services" + ".github/scripts" + ".github/templates" + "docs/sdlc/discovery" + "docs/sdlc/planning" + "docs/sdlc/design" + "docs/sdlc/verification" + "docs/sdlc/release" + "docs/sdlc/operations" + ) + for dir in "${dirs[@]}"; do + mkdir -p "${REPO_ROOT}/${dir}" + done +} + +report_github_structure() { + log "Current .github structure:" + if command -v find >/dev/null 2>&1; then + find "${REPO_ROOT}/.github" -maxdepth 2 -mindepth 1 | sed "s#${REPO_ROOT}/##" | sort + else + log "find command not available; skipping structure scan." + fi +} + +has_python_311_or_higher() { + command -v python3 >/dev/null 2>&1 \ + && python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' +} + +install_python_311() { + if has_python_311_or_higher; then + return 0 + fi + if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then + log "Installing Python 3.11 via apt-get (minimum required: 3.11)." + sudo apt-get update + sudo apt-get install -y python3.11 + else + fail "Python 3.11+ is required. Install it manually or provide apt-get + sudo." + fi + has_python_311_or_higher || fail "Python 3.11+ installation check failed." +} + +has_node_20() { + command -v node >/dev/null 2>&1 \ + && node -e 'const [major] = process.versions.node.split(".").map(Number); process.exit(major >= 20 ? 0 : 1);' +} + +install_node_20() { + if has_node_20; then + return 0 + fi + if command -v apt-get >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then + log "Installing Node.js 20 LTS via NodeSource (minimum required: v20)." + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + else + fail "Node.js v20+ is required. Install it manually or provide apt-get + sudo + curl." + fi + has_node_20 || fail "Node.js v20+ installation check failed." +} + +main() { + require_linux + log "Repository root: ${REPO_ROOT}" + ensure_repo_structure + install_python_311 + install_node_20 + report_github_structure + log "python3: $(python3 --version 2>&1)" + log "node: $(node --version 2>&1)" + log "Bootstrap completed successfully." +} + +main "$@" \ No newline at end of file diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md new file mode 100644 index 0000000..f23db62 --- /dev/null +++ b/.github/skills/release-notes/SKILL.md @@ -0,0 +1,26 @@ +--- +name: release-notes +description: "Use when: release note drafting, deployment communication, change impact summary" +--- + +# Release Notes Skill + +## Goal + +배포 변경사항과 사용자 영향을 이해하기 쉬운 형식으로 전달한다. + +## Workflow + +1. 변경사항을 사용자 영향 기준으로 분류한다. +2. 주의사항/호환성/알려진 이슈를 정리한다. +3. 롤백 또는 대응 가이드를 포함한다. +4. 배포 후 모니터링 포인트를 함께 제공한다. + +## Output Template + +1. Summary +2. What Changed +3. User Impact +4. Migration or Compatibility Notes +5. Known Issues and Workarounds +6. Rollback and Monitoring Notes diff --git a/.github/skills/requirements-refinement/SKILL.md b/.github/skills/requirements-refinement/SKILL.md new file mode 100644 index 0000000..8fdb1d7 --- /dev/null +++ b/.github/skills/requirements-refinement/SKILL.md @@ -0,0 +1,26 @@ +--- +name: requirements-refinement +description: "Use when: vague request, requirement clarification, acceptance criteria drafting" +--- + +# Requirements Refinement Skill + +## Goal + +모호한 요청을 실행 가능한 요구사항과 수용 기준으로 변환한다. + +## Workflow + +1. 문제/목표/제약을 분리해 질문 목록을 만든다. +2. 기능/비기능 요구사항을 구조화한다. +3. 각 요구사항에 수용 기준(AC)을 붙인다. +4. 범위 밖 항목(Out of Scope)을 명시한다. + +## Output Template + +1. Problem Statement +2. Functional Requirements +3. Non-Functional Requirements +4. Acceptance Criteria +5. Out of Scope +6. Open Questions diff --git a/.github/skills/test-design/SKILL.md b/.github/skills/test-design/SKILL.md new file mode 100644 index 0000000..2de4ee3 --- /dev/null +++ b/.github/skills/test-design/SKILL.md @@ -0,0 +1,26 @@ +--- +name: test-design +description: "Use when: test case design, verification planning, regression risk assessment" +--- + +# Test Design Skill + +## Goal + +요구사항과 변경 영향도 기반으로 누락 없는 테스트 시나리오를 만든다. + +## Workflow + +1. 수용 기준을 테스트 관점으로 분해한다. +2. 정상/경계/예외 시나리오를 도출한다. +3. 회귀 영향 영역을 식별한다. +4. 우선순위 기반 실행 순서를 제안한다. + +## Output Template + +1. Test Scope +2. Positive Cases +3. Boundary Cases +4. Negative Cases +5. Regression Targets +6. Execution Priority diff --git a/.github/templates/design-note.template.md b/.github/templates/design-note.template.md new file mode 100644 index 0000000..0aa50f7 --- /dev/null +++ b/.github/templates/design-note.template.md @@ -0,0 +1,71 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Design의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Design 개요 +- 입력 Planning: +- 목적: +- 설계 대상 작업 (WSJF 상위 3개): + - 1순위: + - 2순위: + - 3순위: + +# 2. 설계 대안 비교 (우선순위 상위 작업) +## <작업 ID>: <작업명> +### 대안 A +- 설명: +- 장점: +- 단점: + +### 대안 B +- 설명: +- 장점: +- 단점: + +### 선택 결정 +- 선택: 대안 A / 대안 B +- 근거: + +# 3. 인터페이스 정의 +## <작업 ID>: <작업명> +- 입력: +- 출력: +- 오류 경로: + +# 4. 데이터/흐름 설계 +## <작업 ID>: <작업명> +- 주요 데이터 경로: +- 상태 변화: + +# 5. 테스트 전략 연계 (AC 매핑) +| 작업 ID | AC | 검증 방법 | 검증 포인트 | +|---|---|---|---| +| P-001 | AC-1 | | | +| P-001 | AC-2 | | | + +# 6. 리스크 대응 설계 +| 리스크 ID | 리스크 내용 | 설계 대응 전략 | +|---|---|---| +| R-1 | | | + +# 7. Implementation 단계 전달 체크 +- [ ] 우선순위 상위 작업의 설계 근거(대안 비교 + 선택 이유)가 존재함 +- [ ] 전체 작업의 인터페이스(입력/출력/오류 경로)가 정의됨 +- [ ] AC와 검증 포인트가 매핑됨 +- [ ] Planning 리스크에 대한 설계 대응이 반영됨 +- [ ] 최종 승인 + - {{CONFIRM: Design 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Design 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/discovery-note.template.md b/.github/templates/discovery-note.template.md new file mode 100644 index 0000000..795a106 --- /dev/null +++ b/.github/templates/discovery-note.template.md @@ -0,0 +1,90 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 승인자를 지정하라}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. 계획 상태 요약 +- Discovery 문서 현황: +- Planning 점수 파일 현황: docs/sdlc/planning/planning-backlog-scores.out.csv +- 해석: + +# 2. 요구사항 판정 결과 (완전 중복/유사/신규 + 근거) +- 판정: {{DECIDE: 완전 중복/유사/신규 중 하나를 선택하라}} +- 근거 + - + - + +# 3. 문제점의 요약 +- 현재 상태 + - +- 기대 상태 + - +- 이해관계자 + - +- 제약사항 + - + +# 4. 이번에 만들 기능 1개 +- + +# 5. 요구사항 목록 +## 기능 요구사항 +- FR-1: +- FR-2: + +## 비기능 요구사항 +- NFR-1: +- NFR-2: + +## 범위 경계 +- In Scope + - +- Out of Scope + - + +# 6. 리스크/가정 목록 +## 리스크 +- R-1 (High/Medium/Low): + - 완화 방안: +- R-2 (High/Medium/Low): + - 완화 방안: + +## 가정 +- A-1: +- A-2: + +# 7. 초기 성공 기준 +- S-1: +- S-2: + +## 측정 방식 및 데이터 출처 +- S-1 측정 방식: / 데이터 출처: +- S-2 측정 방식: / 데이터 출처: + +# 8. Planning으로 넘기기 전 확인 체크 +- Planning 시작 시점은 {{DECIDE: 즉시 가능/보류 중 선택하라}}. +- [ ] 범위(In Scope/Out of Scope) 확정 + - 기준: +- [ ] 성공 지표의 측정 방식/데이터 출처 확정 + - 기준: +- [ ] High 영향 플레이스홀더 우선순위 및 해소 책임자 지정 + - 기준: +- [ ] 승인자 확정 + - 기준: + +# 9. 파일 처리 결과 (생성/미생성 경로) +- 처리 결과: +- 생성 경로: +- 재검토: +- 참조 문서: + +# 10. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- (없으면 이 섹션 제거) + +## {{DATA}} +- (없으면 이 섹션 제거) + diff --git a/.github/templates/implementation-note.template.md b/.github/templates/implementation-note.template.md new file mode 100644 index 0000000..32110d9 --- /dev/null +++ b/.github/templates/implementation-note.template.md @@ -0,0 +1,62 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Implementation의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Implementation 개요 +- 입력 Design: +- 목적: +- 이번 구현 우선순위 작업: + - 1순위: + - 2순위: + - 3순위: + +# 2. 이번 구현 범위 +## In Scope +- + +## Out of Scope +- + +# 3. 작업 단위 분해 +| Impl ID | 작업명 | 연결 설계/AC | 변경 유형 | 우선순위 | +|---|---|---|---|---| +| I-001 | | | | | + +# 4. 파일 변경 계획 +| Impl ID | 대상 파일 | 변경 목적 | 변경 종류(생성/수정) | +|---|---|---|---| +| I-001 | | | | + +# 5. 테스트/검증 계획 +| Impl ID | 검증 방법 | 성공 기준 | 근거 | +|---|---|---|---| +| I-001 | | | | + +# 6. 영향도 및 의존성 점검 +| 항목 | 영향 내용 | 대응 방안 | +|---|---|---| +| 코드 | | | +| 문서 | | | +| 설정/의존성 | | | + +# 7. Verification 단계 전달 체크 +- [ ] 구현 범위가 설계 문서와 연결됨 +- [ ] 작업 단위별 검증 방법이 정의됨 +- [ ] 영향 파일과 의존성 점검 결과가 기록됨 +- [ ] Verification에 전달할 실행/검증 근거가 정의됨 +- [ ] 최종 승인 + - {{CONFIRM: Implementation 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Implementation 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/operations-note.template.md b/.github/templates/operations-note.template.md new file mode 100644 index 0000000..bace579 --- /dev/null +++ b/.github/templates/operations-note.template.md @@ -0,0 +1,80 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Operations의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 배포 완료 후 Operations 확인}} + +# 1. 배포 실행 기록 +- 입력 Release: {{DATA: docs/sdlc/release/rel-XXX_YYYY-MM-DD_topic.release.md}} +- 배포 방식: {{DATA: 점진 배포/일괄 배포}} +- 배포 환경: {{DATA: production/staging 등}} +- 배포 창(UTC): {{DATA: 시작/종료 시각}} + +## 사전 검증 +- 체크리스트 점검 결과: {{DATA: 완료/미완료 항목}} +- 롤백 준비 상태: {{DATA: 준비됨/미준비 + 근거}} + +## 실행 결과 +- 실행 명령/절차 요약: {{DATA: 실행 로그 요약}} +- 결과: {{DATA: 성공/부분성공/실패}} +- 영향 범위: {{DATA: 사용자/시스템 영향}} + +## 배포 직후 검증 결과 +- 핵심 시나리오 검증: {{DATA: 통과/실패 + 근거}} +- 예외/알림 발생 여부: {{DATA: 있음/없음 + 내용}} + +# 2. 모니터링 관찰 결과 +| 모니터링 항목 | 측정 지표 | 정상 범위 | 이상 감지 기준 | 관찰 결과 | 비고 | +|---|---|---|---|---|---| +| {{DATA: 항목명}} | {{DATA: 지표}} | {{DATA: 정상 범위}} | {{DATA: 임계치}} | {{DATA: 결과}} | | + +## 관찰 기간 +- 집중 관찰 기간: {{DATA: 시작~종료}} +- 일반 관찰 기간: {{DATA: 시작~종료}} + +# 3. 인시던트 대응 기록 +- 인시던트 발생 여부: {{DATA: 발생/해당없음}} + +## 발생한 경우 기록 +- 영향도 분류: {{DATA: 사용자/기능/데이터 영향}} +- 타임라인: + - 탐지 시각(UTC): {{DATA: 시각}} + - 대응 시작(UTC): {{DATA: 시각}} + - 완화 시각(UTC): {{DATA: 시각}} + - 복구 완료(UTC): {{DATA: 시각}} +- 커뮤니케이션: + - 채널: {{DATA: 채널}} + - 공지 시각(UTC): {{DATA: 시각}} + - 업데이트 주기: {{DATA: 주기}} +- 우회책 실행 여부: {{DATA: 실행/미실행 + 내용}} +- 복구 절차 요약: {{DATA: 복구 단계}} + +# 4. Postmortem +- What Happened: {{DATA: 사건 개요와 사용자 관찰 증상}} +- Impact: {{DATA: 서비스/사용자/비즈니스 영향}} +- Root Cause: {{DATA: 기술적 원인 + 시스템적 원인}} +- Detection: {{DATA: 탐지 경로/지연 요인/알림 품질}} +- Response: {{DATA: 대응 의사결정 및 실행 평가}} +- Recovery: {{DATA: 복구 절차와 검증 결과}} +- Preventive Actions: {{DATA: 재발 방지 액션(담당자/기한 포함)}} +- Follow-up Tracking: {{DATA: Discovery/Planning 백로그 연결 상태}} + +# 5. 다음 반복 환류 항목 +| 환류 ID | 개선 항목 | 연결 대상(Discovery/Planning) | 담당자 | 목표일 | 상태 | +|---|---|---|---|---|---| +| FB-001 | {{DATA: 개선 항목}} | {{DATA: dcy/pln 문서 또는 티켓}} | {{DATA: 담당자}} | {{DATA: YYYY-MM-DD}} | {{DATA: 등록됨/대기}} | + +# 6. 파일 처리 결과 +- 생성/갱신 경로: {{DATA: docs/sdlc/operations/ops-XXX_YYYY-MM-DD_topic.operations.md}} +- 입력 Release 경로: {{DATA: docs/sdlc/release/rel-XXX_YYYY-MM-DD_topic.release.md}} +- 처리 결과: {{DATA: 생성/갱신/검토}} +- 비고: {{DATA: 특이사항}} + +# 7. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- (없으면 이 섹션 제거) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/planning-backlog-scores.template.csv b/.github/templates/planning-backlog-scores.template.csv new file mode 100644 index 0000000..07b685a --- /dev/null +++ b/.github/templates/planning-backlog-scores.template.csv @@ -0,0 +1,4 @@ +item_id,work_item,business_value,time_criticality,rr_oe,job_size,reach,impact,confidence,effort,method,notes +P-001,Example work item A,8,7,6,5,1000,2,80,8,WSJF,baseline sample +P-002,Example work item B,6,8,5,3,3000,1,70,10,RICE,marketing-facing change +P-003,Example work item C,9,6,8,8,500,3,90,5,WSJF,risk reduction heavy diff --git a/.github/templates/planning-note.template.md b/.github/templates/planning-note.template.md new file mode 100644 index 0000000..c33b453 --- /dev/null +++ b/.github/templates/planning-note.template.md @@ -0,0 +1,63 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Planning의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Planning 개요 +- 입력 Discovery: +- 목적: +- 우선순위 방법: WSJF +- 점수 스케일: Business Value/Time Criticality/RR-OE/Job Size 모두 1~10 + +# 2. 이번 반복 범위 경계 +## In Scope +- + +## Out of Scope +- + +# 3. 백로그 작업 분해 +| Item ID | Work Item | 연결 요구사항 | 연결 리스크 | 담당 | +|---|---|---|---|---| +| P-001 | | | | | + +# 4. WSJF 점수표 +| Item ID | Work Item | Business Value | Time Criticality | RR/OE | Job Size | WSJF Score | Priority | +|---|---:|---:|---:|---:|---:|---:|---:| +| P-001 | | | | | | | | + +계산식: WSJF = (Business Value + Time Criticality + RR/OE) / Job Size + +## 점수 근거 메모 +- P-001: + +# 5. 작업별 수용 기준 (AC) +## P-001 AC +- AC-1: +- AC-2: + +# 6. 우선순위 조정/예외 +- 동률 처리: +- Override: +- 승인자: + +# 7. Design 단계 전달 체크 +- [ ] 우선순위 상위 백로그의 AC가 검증 가능한 문장으로 정의됨 +- [ ] WSJF 계산 근거 기록 완료 +- [ ] 이번 반복 In Scope/Out of Scope 확정 +- [ ] 최종 승인 + - {{CONFIRM: Planning 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: +- 점수 파일 갱신: docs/sdlc/planning/planning-backlog-scores.out.csv + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Planning 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/planning-scoring-sheet.template.csv b/.github/templates/planning-scoring-sheet.template.csv new file mode 100644 index 0000000..7dbd2be --- /dev/null +++ b/.github/templates/planning-scoring-sheet.template.csv @@ -0,0 +1,4 @@ +item_id,work_item,business_value,time_criticality,rr_oe,job_size,reach,impact,confidence,effort,method,wsjf_score,rice_score,selected_score,priority +P-001,Example work item A,8,7,6,5,1000,2,80,8,WSJF,=(C2+D2+E2)/F2,=(G2*H2*I2)/J2,=IF(K2="WSJF",L2,M2),= +P-002,Example work item B,6,8,5,3,3000,1,70,10,RICE,=(C3+D3+E3)/F3,=(G3*H3*I3)/J3,=IF(K3="WSJF",L3,M3),= +P-003,Example work item C,9,6,8,8,500,3,90,5,WSJF,=(C4+D4+E4)/F4,=(G4*H4*I4)/J4,=IF(K4="WSJF",L4,M4),= diff --git a/.github/templates/release-note.template.md b/.github/templates/release-note.template.md new file mode 100644 index 0000000..4a6410d --- /dev/null +++ b/.github/templates/release-note.template.md @@ -0,0 +1,105 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Release의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Release 개요 +- 입력 Verification: +- 배포 대상 범위: +- 배포 방식: {{DECIDE: 점진 배포/일괄 배포 중 선택하라}} +- 배포 환경: + +# 2. 배포 체크리스트 + +| 항목 | 확인 내용 | 결과 (완료/미완료/해당없음) | 비고 | +|---|---|---|---| +| 설정 검증 | 환경 변수, 시크릿, 외부 연동 키 유효성 확인 | | | +| 데이터 검증 | 마이그레이션 순서, 백업 상태, 복구 가능성 확인 | | | +| 호환성 검증 | 이전 버전 클라이언트/API 호환 여부 확인 | | | +| 의존성 검증 | 신규/변경 라이브러리 버전 충돌 여부 확인 | | | +| 트래픽 전략 | 점진/일괄 배포 방식과 중단 기준 확인 | | | +| 커뮤니케이션 | 사용자 공지, 내부 공지, 온콜 담당자 공유 완료 | | | + +# 3. 롤백 계획 + +## 롤백 트리거 +- 에러율 임계치: +- 지연 임계치: +- 장애 지표: + +## 롤백 절차 +1. +2. +3. + +## 데이터 롤백 전략 +- 스키마/데이터 복원 여부: +- 절차: + +## 롤백 후 검증 +- 핵심 시나리오 확인 항목: + - + +## 롤백 커뮤니케이션 +- 공지 채널: +- 공지 시각: +- 상태 업데이트 주기: + +## 참조 런북 +- 롤백 런북: .github/templates/rollback-runbook.template.md + +# 4. 릴리즈 노트 초안 + +## 변경 요약 +- + +## 주요 변경 내용 +- + +## 영향 범위 +- 사용자 영향: +- 시스템 영향: + +## 알려진 제한 사항 +- + +# 5. 배포 후 관찰 계획 + +| 모니터링 항목 | 측정 지표 | 정상 범위 | 이상 감지 기준 | 담당자 | +|---|---|---|---|---| +| | | | | | + +## 관찰 기간 +- 집중 관찰 기간: +- 일반 관찰 기간: + +# 6. 승인된 예외 인계 목록 + +| 예외 ID | 출처 (Verification) | 심각도 | 완화책 | 유효 기간 (UTC) | Operations 후속 조치 | +|---|---|---|---|---|---| +| | | | | | | + +> 예외 없음 시 이 섹션을 "해당 없음"으로 처리한다. + +# 7. Operations 단계 전달 체크 +- [ ] 롤백 가능한 배포 계획 승인됨 +- [ ] 배포 체크리스트 항목 전체 "완료" 또는 "해당없음" 처리됨 +- [ ] 릴리즈/롤백 체크리스트가 근거와 함께 완료됨 +- [ ] 배포 후 관찰 항목이 정의됨 +- [ ] Operations에 전달할 관찰 포인트, 장애 트리거, 후속 대응 절차 준비됨 +- [ ] 승인된 예외 이력 인계됨 +- [ ] 최종 승인 + - {{CONFIRM: Release 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Release 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/templates/rollback-runbook.template.md b/.github/templates/rollback-runbook.template.md new file mode 100644 index 0000000..d8fceb5 --- /dev/null +++ b/.github/templates/rollback-runbook.template.md @@ -0,0 +1,62 @@ +# Rollback Runbook Template + +## Metadata + +1. Change ID: +2. Release Version: +3. Environment: +4. Owner: +5. On-call Contact: +6. Last Updated (UTC): + +## Trigger Conditions + +1. Error Rate Threshold: +2. Latency Threshold: +3. Availability Threshold: +4. Data Integrity Alarm: +5. Business Impact Trigger: + +## Pre-rollback Checks + +1. 장애 범위 확인(기능/사용자/데이터) +2. 최근 배포 변경점 확인 +3. 백업/스냅샷 가용성 확인 +4. 커뮤니케이션 채널 준비 + +## Rollback Procedure + +1. 트래픽 차단 또는 점진 축소 +2. 이전 안정 버전으로 아티팩트 전환 +3. 마이그레이션 롤백 또는 안전 모드 전환 +4. 캐시/큐/비동기 작업 상태 정리 +5. 서비스 재기동 및 헬스체크 확인 + +## Data Recovery Plan + +1. 복원 대상 데이터 범위: +2. 복원 방법(백업/스냅샷/리플레이): +3. 데이터 무결성 검증 방법: +4. 복원 예상 시간: + +## Validation After Rollback + +1. 핵심 사용자 시나리오 통과 여부 +2. API 오류율 정상화 여부 +3. 지연 시간 정상화 여부 +4. 데이터 정합성 점검 결과 +5. 모니터링 알림 해소 여부 + +## Communication Plan + +1. 롤백 선언 메시지(내부) +2. 영향 공지 메시지(외부) +3. 상태 업데이트 주기 +4. 종료 공지 조건 + +## Post-rollback Follow-up + +1. 원인 분석 티켓: +2. 재배포 조건: +3. 임시 완화책 제거 계획: +4. Postmortem 일정: diff --git a/.github/templates/verification-note.template.md b/.github/templates/verification-note.template.md new file mode 100644 index 0000000..fd73a54 --- /dev/null +++ b/.github/templates/verification-note.template.md @@ -0,0 +1,83 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Verification의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Verification 개요 +- 입력 Implementation: +- 목적: +- 검증 대상 작업: + - 1순위: + - 2순위: + - 3순위: + +# 2. 검증 범위 +## In Scope +- + +## Out of Scope +- + +# 3. AC별 검증 결과 + +| Impl ID | AC | 검증 방법 | 결과 (통과/실패/보류) | 비고 | +|---|---|---|---|---| +| I-001 | | | | | + +## 품질 게이트 판정 +- Blocker 결함 수: +- Major 결함 수: +- 승인된 예외 건수: +- 배포 가능 여부: {{DECIDE: 배포 가능/불가 중 선택하라}} + +# 4. 결함 목록 + +| 결함 ID | 연결 Impl ID | 심각도 (Blocker/Major/Minor) | 증상 | 재현 여부 | 처리 상태 | +|---|---|---|---|---|---| +| D-001 | | | | | | + +## 심각도 기준 +- Blocker: 서비스 중단, 데이터 손실/오염, 보안 취약점 악용 가능 상태 +- Major: 핵심 기능 오동작, 우회 가능하지만 사용자 영향이 큰 결함 +- Minor: 비핵심 기능 이슈, 문구/표시 오류, 영향이 제한적인 결함 + +# 5. 회귀 테스트 결과 + +| 테스트 항목 | 영향 영역 | 실행 결과 | 실패 시 원인 | +|---|---|---|---| +| | | | | + +## 회귀 리스크 평가 +- 영향 영역: +- 롤백 조건: + +# 6. 예외 승인 목록 + +| 예외 ID | 연결 결함 | 심각도 | 완화책 | 승인자 | 승인 시각 (UTC) | 유효 기간 (UTC) | +|---|---|---|---|---|---|---| +| | | | | | | | + +> 예외 없음 시 이 섹션을 "해당 없음"으로 처리한다. + +# 7. Release 단계 전달 체크 +- [ ] Blocker 결함 0건 확인 +- [ ] Major 결함 0건 또는 승인된 예외 존재 +- [ ] 변경 코드 경로의 테스트 실행 증거(로그/리포트) 첨부 +- [ ] 신규 기능의 정상 경로 테스트 1개 이상 및 예외 경로 테스트 1개 이상 확인 +- [ ] 회귀 테스트 실패 없음 또는 원인 분석/롤백 계획 존재 +- [ ] Release에 전달할 배포 가능 여부, 예외 승인 정보, 잔여 리스크 준비됨 +- [ ] 최종 승인 + - {{CONFIRM: Verification 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- (없으면 이 섹션 제거) + +## {{CONFIRM}} +- Verification 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- (없으면 이 섹션 제거) diff --git a/.github/tools/calc_priority_scores.sh b/.github/tools/calc_priority_scores.sh new file mode 100755 index 0000000..0268819 --- /dev/null +++ b/.github/tools/calc_priority_scores.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [output_csv]" + exit 1 +fi + +infile="$1" +outfile="${2:-}" + +tmp_scored="$(mktemp)" +tmp_sorted="$(mktemp)" +trap 'rm -f "$tmp_scored" "$tmp_sorted"' EXIT + +awk -F',' 'BEGIN { OFS="," } +NR==1 { + print "item_id","work_item","method","wsjf_score","rice_score","selected_score","notes" + next +} +{ + item=$1 + work=$2 + bv=$3+0 + tc=$4+0 + rroe=$5+0 + js=$6+0 + reach=$7+0 + impact=$8+0 + conf=$9+0 + effort=$10+0 + method=toupper($11) + notes=$12 + + wsjf=(js==0)?0:((bv+tc+rroe)/js) + rice=(effort==0)?0:((reach*impact*conf)/effort) + selected=(method=="RICE")?rice:wsjf + + printf "%s,%s,%s,%.4f,%.4f,%.4f,%s\n", item, work, method, wsjf, rice, selected, notes +} +' "$infile" > "$tmp_scored" + +{ + head -n 1 "$tmp_scored" + tail -n +2 "$tmp_scored" | sort -t',' -k6,6gr +} > "$tmp_sorted" + +awk -F',' 'BEGIN { OFS="," } +NR==1 { + print $0, "priority" + next +} +{ + print $0, NR-1 +} +' "$tmp_sorted" > "${outfile:-/dev/stdout}" From cc66e5f9a1aaa12d96801323b54204be2977397f Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 22:31:25 +0900 Subject: [PATCH 12/21] docs: update local setup and add phub CLI alias --- .env.example | 8 ++++---- .pre-commit-config.yaml | 9 --------- README.md | 39 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 4 files changed, 43 insertions(+), 14 deletions(-) delete mode 100644 .pre-commit-config.yaml diff --git a/.env.example b/.env.example index 507773e..b8b02f8 100644 --- a/.env.example +++ b/.env.example @@ -6,7 +6,7 @@ PORTFOLIOHUB_ADMIN_TOKENS=admin-token-789 PORTFOLIOHUB_ENVIRONMENT=development # Enable DB-backed repository (uses TIMESCALEDB_* below) -PORTFOLIOHUB_DB_ENABLED=false +PORTFOLIOHUB_DB_ENABLED=true PORTFOLIOHUB_BOOTSTRAP_USERNAME= PORTFOLIOHUB_BOOTSTRAP_EMAIL= PORTFOLIOHUB_BOOTSTRAP_PASSWORD= @@ -17,9 +17,9 @@ PORTFOLIOHUB_BOT_TOKEN_DEFAULT_TTL_DAYS=365 # TimescaleDB / PostgreSQL connection settings TIMESCALEDB_HOST=localhost TIMESCALEDB_PORT=5432 -TIMESCALEDB_NAME=portfoliohub -TIMESCALEDB_USER=portfoliohub -TIMESCALEDB_PASSWORD= +TIMESCALEDB_NAME=portfolio_hub +TIMESCALEDB_USER=portfolio_hub_user +TIMESCALEDB_PASSWORD=change-me TIMESCALEDB_SSLMODE=prefer TIMESCALEDB_CONNECT_TIMEOUT_SEC=10 TIMESCALEDB_APPLICATION_NAME=portfoliohub diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index fad8953..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -repos: - - repo: local - hooks: - - id: docs-sync-check - name: docs-sync-check - entry: python3 scripts/check_docs_sync.py --staged - language: system - pass_filenames: false - stages: [pre-commit] diff --git a/README.md b/README.md index ad00e9f..f1f4bab 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ For complete configuration options, see `.env.example`. ```bash python -m venv .venv source .venv/bin/activate -pip install -e .[dev] +python -m pip install -e '.[dev]' # Configure .env cp .env.example .env @@ -57,9 +57,46 @@ uvicorn app.main:app --reload pytest ``` +If you need local infrastructure for TimescaleDB, Redis, and Loki, start them with Docker Compose before running the app: + +```bash +docker compose -f docker-compose.local-test.yml up -d +docker compose -f docker-compose.local-test.yml ps +``` + +Default endpoints exposed by `docker-compose.local-test.yml`: + +- TimescaleDB: `127.0.0.1:5432` +- Redis: `127.0.0.1:6379` (RedisInsight UI: `127.0.0.1:8001`) +- Loki: `127.0.0.1:3100` + +Typical `.env` values for this local setup: + +```env +PORTFOLIOHUB_DB_ENABLED=true +TIMESCALEDB_HOST=127.0.0.1 +TIMESCALEDB_PORT=5432 +TIMESCALEDB_USER=portfolio_hub_user +TIMESCALEDB_PASSWORD=change-me +TIMESCALEDB_NAME=portfolio_hub + +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 + +LOKI_HOST=127.0.0.1 +LOKI_PORT=3100 +``` + +To stop and remove the local containers: + +```bash +docker compose -f docker-compose.local-test.yml down +``` + ## CLI The repository now includes a `portfoliohub` CLI that mirrors the main portfolio flows from `jsbot-kis`. +The same commands are also available through the shorter alias `phub`. ```bash portfoliohub --help diff --git a/pyproject.toml b/pyproject.toml index 2ecda84..b467d3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ [project.scripts] portfoliohub = "app.cli.main:main" +phub = "app.cli.main:main" [project.optional-dependencies] dev = [ From a701c98338199785eb5779f75ee32f55e894655c Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 23:43:47 +0900 Subject: [PATCH 13/21] draft --- ...14_cli-bot-token-issue-delete.discovery.md | 109 ++++++++++++++++++ .../planning/planning-backlog-scores.out.csv | 6 + ...14_cli-bot-token-lifecycle-cli.planning.md | 101 ++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 docs/sdlc/discovery/dcy-001_2026-04-14_cli-bot-token-issue-delete.discovery.md create mode 100644 docs/sdlc/planning/planning-backlog-scores.out.csv create mode 100644 docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md diff --git a/docs/sdlc/discovery/dcy-001_2026-04-14_cli-bot-token-issue-delete.discovery.md b/docs/sdlc/discovery/dcy-001_2026-04-14_cli-bot-token-issue-delete.discovery.md new file mode 100644 index 0000000..95176b6 --- /dev/null +++ b/docs/sdlc/discovery/dcy-001_2026-04-14_cli-bot-token-issue-delete.discovery.md @@ -0,0 +1,109 @@ +# 0. 문서 상태 +- 상태: confirmed +- 승인자: Justin +- 승인 시각: 2026-04-14 + +# 1. 계획 상태 요약 +- Discovery 문서 현황: 본 문서가 첫 Discovery 초안이다. +- Planning 점수 파일 현황: docs/sdlc/planning/planning-backlog-scores.out.csv (존재하지 않음) +- 해석: CLI에서 봇 토큰 발급/삭제를 지원하기 위한 요구사항 정의가 우선이며, Planning 단계에서 만기 정책과 우선순위 점수화를 확정해야 한다. + +# 2. 요구사항 판정 결과 (완전 중복/유사/신규 + 근거) +- 판정: 신규 +- 근거 + - docs/sdlc/discovery/ 경로에 기존 Discovery 문서가 없어 완전 중복/유사 판정을 수행할 대상이 없다. + - 현재 저장소의 CLI는 user/portfolio/strategy 중심이며, bot 토큰 발급/삭제를 직접 수행하는 CLI 커맨드가 없다. + +# 3. 문제점의 요약 +- 현재 상태 + - 인증 API는 봇/봇 토큰 기능을 제공하지만, 운영자가 CLI만으로 봇 토큰 발급/삭제를 실행할 수 없다. + - 자동화 스크립트에서 API 직접 호출 의존이 발생해 운영 절차 일관성이 낮다. +- 기대 상태 + - CLI에서 특정 사용자(또는 사용자 소유 봇)에 대해 봇 토큰 발급/삭제를 일관된 명령으로 처리할 수 있어야 한다. + - 토큰 만기 설정 정책은 선택 입력으로 하되, 미입력 시 기본 TTL 365일을 적용하고 운영상 필요 시 `--no-expiry` 옵션으로 무기한 발급을 허용해야 한다. +- 이해관계자 + - 운영자(관리자 권한 사용자): 봇 토큰 발급/폐기의 실행 주체. + - 자동화/배포 담당자: 스크립트 기반 토큰 라이프사이클 관리 필요. + - 보안/승인 책임자: 토큰 만기 정책과 최소 권한 정책 승인 필요. +- 제약사항 + - 기존 권한 모델(admin/read/trade-write) 및 auth 서비스 계약과 호환되어야 한다. + - 기존 CLI UX 패턴(확인 프롬프트, --force, 출력 포맷 table/json)을 유지해야 한다. + - 저장소의 문서 동기화 규칙에 따라 API/CLI 계약 변경 시 기술 문서 업데이트가 필요하다. + +# 4. 이번에 만들 기능 1개 +- CLI에서 봇 토큰 발급/삭제 기능을 제공하고, 토큰 만기 정책(필수/기본값)을 명시적으로 처리한다. + +# 5. 요구사항 목록 +## 기능 요구사항 +- FR-1: 관리자는 CLI에서 봇 토큰을 발급할 수 있어야 한다 (대상 bot 식별자 지정 필수). +- FR-2: 관리자는 CLI에서 봇 토큰을 삭제(폐기)할 수 있어야 한다 (대상 token 식별자 지정 필수). +- FR-3: 발급 시 만기 정책을 입력 또는 기본값으로 처리해야 하며, 기본값은 365일 TTL이고 명시적 `--no-expiry` 지정 시에만 무기한 발급을 허용해야 한다. +- FR-4: 발급/삭제 명령은 기존 쓰기 명령과 동일하게 확인 프롬프트를 제공하고, --force 시 프롬프트를 생략해야 한다. +- FR-5: 명령 결과는 기존 CLI 포맷 정책(table/json)으로 출력되어야 하며, token_id, expires_at, revoked 상태를 포함해야 한다. + +## 비기능 요구사항 +- NFR-1: 보안 - 토큰 원문은 발급 시 1회만 노출하고, 이후 조회/출력 시 앞 6자/뒤 4자만 노출하는 마스킹 정책을 따른다. +- NFR-2: 감사추적 - 발급/삭제 행위에 대해 실행자, 대상 bot_id, 시각, 결과를 로그로 남겨야 한다. +- NFR-3: 사용성 - 기존 CLI 하위 명령 체계와 일관된 help 텍스트/옵션명을 유지해야 한다. +- NFR-4: 신뢰성 - 잘못된 bot_id/token_id 입력 시 비정상 종료 대신 명확한 오류 메시지와 종료 코드를 제공해야 한다. + +## 범위 경계 +- In Scope + - CLI에 bot 토큰 발급/삭제 명령 추가. + - 만기 입력 옵션 및 기본 TTL 연동. + - 확인 프롬프트/--force/출력 포맷 일관화. + - 관련 기술 문서(API/CLI 계약) 업데이트. +- Out of Scope + - 토큰 암호화 저장 구조 전면 재설계. + - 다중 사용자 셀프서비스 포털 UI 제공. + - 외부 IAM 연동(OAuth/OIDC 등) 추가. + +# 6. 리스크/가정 목록 +## 리스크 +- R-1 (High): 만기 정책이 모호하면 무기한 토큰 발급으로 보안 리스크가 커질 수 있다. + - 완화 방안: 기본 TTL 상한을 365일로 강제하고, 무기한 발급은 명시적 `--no-expiry` 옵션과 관리자 권한에서만 허용하며 감사 로그를 남긴다. +- R-2 (Medium): CLI 명령 체계 확장 시 기존 명령 네이밍과 충돌하여 운영자가 오사용할 수 있다. + - 완화 방안: 기존 command group 패턴에 맞춰 bot 하위 명령을 도입하고 --help 예시를 README에 추가한다. +- R-3 (Medium): 토큰 출력 정책이 미정이면 로그/스크린샷을 통한 민감정보 유출 가능성이 있다. + - 완화 방안: 발급 직후 1회 노출 정책과 마스킹 정책을 적용하고 운영 가이드에 비노출 원칙을 명시한다. + +## 가정 +- A-1: 봇/봇토큰 관련 API 및 서비스 계층은 이미 존재하며 CLI는 이를 호출하는 어댑터 역할로 구현 가능하다. +- A-2: 운영 환경에서 bot 토큰 발급/폐기 실행 권한은 admin으로 제한된다. + +# 7. 초기 성공 기준 +- S-1: 관리자가 CLI로 bot 토큰 발급/삭제를 수행할 수 있다. +- S-2: 만기 정책(필수 또는 기본 TTL)이 문서와 구현에서 일치한다. +- S-3: 발급/삭제 명령의 정상/예외 경로 검증 증거가 Verification에 제출된다. + +## 측정 방식 및 데이터 출처 +- S-1 측정 방식: 통합 테스트 또는 수동 E2E 시나리오에서 발급 후 토큰 사용 가능, 삭제 후 사용 불가 확인 / 데이터 출처: CLI 실행 로그, API 응답 로그. +- S-2 측정 방식: 만기 입력 유무별 테스트 케이스 통과 여부 확인 / 데이터 출처: 테스트 리포트, 설정값(.env) 및 문서 diff. +- S-3 측정 방식: 정상 1건 + 예외 1건 이상 테스트 증적 첨부 여부 확인 / 데이터 출처: pytest 결과, Verification 문서. + +# 8. Planning으로 넘기기 전 확인 체크 +- Planning 시작 시점은 보류. +- [x] 범위(In Scope/Out of Scope) 확정 + - 기준: 본 문서 #5 범위 경계에 포함/제외 항목이 명시되어 있다. +- [x] 성공 지표의 측정 방식/데이터 출처 확정 + - 운영 지표 수집 채널: "운영자는 CLI 실행 결과를 터미널 출력으로 직접 확인하며, 별도 중앙 로그/대시보드는 없음. 추후 자동화/감사 필요 시 /var/log/portfoliohub/cli.log에 기록 예정." +- [x] High 영향 플레이스홀더 우선순위 및 해소 책임자 지정 + - 기준: [x] 미해결 High 영향 플레이스홀더 없음 +- [x] 승인자 확정 + - 기준: 승인자가 Justin으로 명시되어 있다. + +# 9. 파일 처리 결과 (생성/미생성 경로) +- 처리 결과: 신규 생성 +- 생성 경로: docs/sdlc/discovery/dcy-001_2026-04-14_cli-bot-token-issue-delete.discovery.md +- 재검토: Planning 이전에 운영 지표 수집 채널 확정 필요 +- 참조 문서: 없음 + +# 10. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- 없음 + +## {{CONFIRM}} +- 승인 시각 기재. + +## {{DATA}} +- 없음 diff --git a/docs/sdlc/planning/planning-backlog-scores.out.csv b/docs/sdlc/planning/planning-backlog-scores.out.csv new file mode 100644 index 0000000..ebd4641 --- /dev/null +++ b/docs/sdlc/planning/planning-backlog-scores.out.csv @@ -0,0 +1,6 @@ +item_id,work_item,business_value,time_criticality,rr_oe,job_size,reach,impact,confidence,effort,method,notes +P-002,만기 정책 및 권한 가드 구현 (--no-expiry 및 기본 TTL 365일),9,10,9,4,,,,,WSJF,"WSJF=7.00; Priority=1; FR-3/R-1" +P-003,토큰 출력 마스킹 및 감사 로그 구현,8,8,10,4,,,,,WSJF,"WSJF=6.50; Priority=2; NFR-1/NFR-2/R-3" +P-001,CLI bot 토큰 발급/삭제 명령 추가,10,9,8,5,,,,,WSJF,"WSJF=5.40; Priority=3; FR-1/FR-2/FR-4/FR-5" +P-004,CLI UX 정합성 보강 (confirm-force/오류/출력 포맷),7,7,7,4,,,,,WSJF,"WSJF=5.25; Priority=4; NFR-3/NFR-4" +P-005,테스트 및 문서 동기화,7,6,8,5,,,,,WSJF,"WSJF=4.20; Priority=5; S-1/S-2/S-3" diff --git a/docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md b/docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md new file mode 100644 index 0000000..f732f11 --- /dev/null +++ b/docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md @@ -0,0 +1,101 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: Justin +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Planning 개요 +- 입력 Discovery: dcy-001 +- 목적: CLI에서 bot 토큰 발급/삭제 기능을 우선순위 기반 작업으로 분해하고 Design 단계 전달 가능한 AC를 확정한다. +- 우선순위 방법: WSJF +- 점수 스케일: Business Value/Time Criticality/RR-OE/Job Size 모두 1~10 + +# 2. 이번 반복 범위 경계 +## In Scope +- CLI `bot` 하위 명령에 토큰 발급/삭제 기능 추가 +- 만기 정책 반영(기본 TTL 365일, `--no-expiry` 옵션 처리) +- 토큰 출력 마스킹(앞 6자/뒤 4자) 및 감사 로그 반영 +- 확인 프롬프트/`--force`/출력 포맷(`table`/`json`) 일관화 +- 테스트(정상/예외) 및 기술 문서 동기화 + +## Out of Scope +- 토큰 저장 구조 전면 재설계 +- UI/포털 제공 +- 외부 IAM(OAuth/OIDC) 연동 + +## 운영/보안 정책 +- 운영 정책: 터미널 접근은 오직 관리자만 접근 가능하며 일반 유저는 터미널 사용이 불가하다. 따라서 CLI 사용은 관리자 행위로 간주하며, 관리자만 터미널 접근 및 CLI 실행 권한을 가진다. +- 보안 정책: `--no-expiry` 옵션은 관리자 전용 운영 정책 하에서만 허용된다. + +# 3. 백로그 작업 분해 +| Item ID | Work Item | 연결 요구사항 | 연결 리스크 | 담당 | +|---|---|---|---|---| +| P-002 | 만기 정책 및 권한 가드 구현 (`--no-expiry`, TTL 기본 365일, 관리자 전용 정책) | FR-3, NFR-4 | R-1 | Builder | +| P-003 | 토큰 출력 마스킹 및 감사 로그 구현 | NFR-1, NFR-2 | R-1, R-3 | Builder | +| P-001 | CLI bot 토큰 발급/삭제 명령 추가 | FR-1, FR-2, FR-4, FR-5 | R-2 | Builder | +| P-004 | CLI UX 정합성 보강(확인 프롬프트, `--force`, 오류 메시지) | FR-4, NFR-3, NFR-4 | R-2 | Builder | +| P-005 | 테스트/문서 동기화(README, API/CLI 계약) | S-1, S-2, S-3 | R-2, R-3 | Builder | + +# 4. WSJF 점수표 +| Item ID | Work Item | Business Value | Time Criticality | RR/OE | Job Size | WSJF Score | Priority | +|---|---:|---:|---:|---:|---:|---:|---:| +| P-002 | 만기 정책 및 권한 가드 구현 | 9 | 10 | 9 | 4 | 7.00 | 1 | +| P-003 | 토큰 출력 마스킹 및 감사 로그 구현 | 8 | 8 | 10 | 4 | 6.50 | 2 | +| P-001 | CLI bot 토큰 발급/삭제 명령 추가 | 10 | 9 | 8 | 5 | 5.40 | 3 | +| P-004 | CLI UX 정합성 보강 | 7 | 7 | 7 | 4 | 5.25 | 4 | +| P-005 | 테스트/문서 동기화 | 7 | 6 | 8 | 5 | 4.20 | 5 | + +계산식: WSJF = (Business Value + Time Criticality + RR/OE) / Job Size + +## 점수 근거 메모 +- P-002: 무기한 발급 오남용 리스크(R-1)를 직접 줄이는 핵심 정책 구현이라 TC/RR-OE를 높게 반영했다. +- P-003: 민감정보 노출 리스크(R-3)와 감사추적 요구(NFR-2)를 직접 완화하므로 RR/OE를 최상위로 반영했다. +- P-001: 기능 핵심(발급/삭제)이지만 정책/보안 가드 선행 필요성이 있어 우선순위를 3으로 배치했다. +- P-004: 운영 UX 일관성 확보에 필요하나 기능/보안 블로커 대비 사업가치는 상대적으로 낮아 후순위다. +- P-005: 품질 게이트 통과 및 문서 동기화 필수이나 구현 선행 항목 의존이 있어 마지막 순위로 배치했다. + +# 5. 작업별 수용 기준 (AC) +## P-002 AC +- AC-1: 토큰 발급 명령에서 만기 미입력 시 기본 TTL 365일이 적용된다. +- AC-2: `--no-expiry` 옵션은 관리자 전용 CLI 운영 정책에서만 허용되며, 정책 비적합 실행에서는 명확한 오류 코드/메시지를 반환한다. +- AC-3: 만기 관련 옵션 조합이 잘못된 경우(예: TTL과 no-expiry 동시 지정) 검증 오류가 발생한다. + +## P-003 AC +- AC-1: 토큰 원문은 발급 직후 1회만 출력되고 이후 출력/조회에서는 앞 6자/뒤 4자 외 마스킹된다. +- AC-2: 발급/삭제 시 실행자, bot_id, token_id(마스킹), 시각, 결과가 감사 로그에 기록된다. +- AC-3: 로그에는 토큰 원문이 저장되지 않는다. + +## P-001 AC +- AC-1: CLI에 bot 토큰 발급 명령과 삭제 명령이 추가되고 `--help`에서 확인 가능하다. +- AC-2: 발급 명령 성공 시 `token_id`, `expires_at`, `revoked` 정보가 출력 포맷 정책에 맞게 표시된다. +- AC-3: 삭제 명령 성공 시 토큰이 즉시 폐기되어 재사용이 불가능해야 한다. + +## P-004 AC +- AC-1: 발급/삭제 명령은 기본적으로 확인 프롬프트를 표시하고 `--force` 사용 시 프롬프트를 생략한다. +- AC-2: 잘못된 bot_id/token_id 입력 시 사용자 행동 지침이 포함된 오류 메시지를 제공한다. +- AC-3: `table`/`json` 출력 모드에서 동일한 필수 필드 집합을 보장한다. + +## P-005 AC +- AC-1: 신규 기능에 대해 정상 경로 1건 이상, 예외 경로 1건 이상의 테스트가 추가된다. +- AC-2: docs sync 규칙 대상 문서(README, API/CLI 계약)가 코드 변경과 함께 갱신된다. +- AC-3: Verification 단계에서 테스트 실행 증적을 첨부할 수 있는 로그/리포트가 생성된다. + +# 6. 우선순위 조정/예외 +- 동률 처리: WSJF 동률 시 RR/OE가 큰 항목 우선, 동률 지속 시 Job Size가 작은 항목 우선. +- Override: 없음 +- 승인자: Justin + +# 7. Design 단계 전달 체크 +- [x] 우선순위 상위 백로그의 AC가 검증 가능한 문장으로 정의됨 +- [x] WSJF 계산 근거 기록 완료 +- [x] 이번 반복 In Scope/Out of Scope 확정 +- [ ] 최종 승인 + - {{CONFIRM: Planning 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md +- 점수 파일 갱신: docs/sdlc/planning/planning-backlog-scores.out.csv + +# 9. 사용자 결정 필요 항목 요약 +## {{CONFIRM}} +- 승인 시각 기재. (위치: #0) +- Planning 초안 승인 여부를 확인하라. (위치: #7) From 4bef0889334e6e1267e22bfc97998ea20eb473f1 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 23:44:02 +0900 Subject: [PATCH 14/21] Squashed '.vendor/stage-pilot/' changes from edf7e61..575a73c 575a73c chore: refine planning review matching and startup guide git-subtree-dir: .vendor/stage-pilot git-subtree-split: 575a73ce74df5c33ba3c7f54c81d2102df95ebd0 --- .github/START-HERE.md | 1 - .github/prompts/planning-review.prompt.md | 28 +++++++++++++---------- .vscode/settings.json | 5 ++++ 3 files changed, 21 insertions(+), 13 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.github/START-HERE.md b/.github/START-HERE.md index 118f035..0e04da7 100644 --- a/.github/START-HERE.md +++ b/.github/START-HERE.md @@ -16,7 +16,6 @@ 2. 파일명 형식: dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md 3. 날짜는 생성일 기준으로 유지 4. 작성 템플릿: .github/templates/discovery-note.template.md -5. 문서 하단 Discovery Freeze 필수 ## 2. Planning Scoring Quick Start diff --git a/.github/prompts/planning-review.prompt.md b/.github/prompts/planning-review.prompt.md index f01fa02..6ef0cdf 100644 --- a/.github/prompts/planning-review.prompt.md +++ b/.github/prompts/planning-review.prompt.md @@ -1,26 +1,28 @@ --- -description: "Use when: /planning-review to re-review existing planning documents and update them in place from Discovery IDs" +description: "Use when: /planning-review to re-review existing planning documents and update them in place from Planning IDs" name: "planning-review" -argument-hint: "Enter Discovery IDs separated by commas. Example: dcy-001, dcy-002" +argument-hint: "Enter Planning IDs separated by commas. Example: pln-001, pln-002" agent: "agent" model: "reviewer" tools: [read, edit, search, todo, agent] --- 당신은 SDLC Planning 문서 재검토 도우미다. -사용자가 지정한 Discovery 문서 ID 목록을 기준으로 Planning 문서를 재검토하고, 필요한 보정을 반영해 저장한다. +사용자가 지정한 Planning 문서 ID 목록을 기준으로 Planning 문서를 재검토하고, 필요한 보정을 반영해 저장한다. 반드시 아래 규칙을 지켜라. 1. 출력은 한국어로 작성한다. -2. 입력은 쉼표(,) 기준으로 분리해 Discovery ID 목록으로 처리한다. +2. 입력은 쉼표(,) 기준으로 분리해 Planning ID 목록으로 처리한다. 3. 입력 정규화 규칙: - 공백 제거 - - 소문자 정규화(dcy-001 형식) + - 소문자 정규화(pln-001 형식) - 중복 ID 제거 -4. ID 형식은 dcy-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +4. ID 형식은 pln-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. 5. Planning 파일 검색 규칙: - - 1차: dcy-001 -> docs/sdlc/planning/pln-001_*.planning.md 패턴 검색 - - 2차: 1차 미발견 시 docs/sdlc/planning/*.planning.md에서 `입력 Discovery:` 줄에 dcy-001 포함 문서 검색 + - 1차: pln-001 -> docs/sdlc/planning/pln-001_*.planning.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/planning/*.planning.md에서 파일명에 pln-001 토큰이 포함된 문서만 후보로 수집한다. + - 후보 문서는 `입력 Planning ID:` 필드 값이 pln-001과 정확히 일치할 때만 매칭한다. 필드가 없거나 불일치하면 매칭하지 않는다. + - 본문 단순 포함(contains) 검색만으로는 매칭하지 않는다. 6. 존재하지 않는 Planning 문서는 생성하지 않는다. - 미발견 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. 7. 점수 산출물 정책: @@ -32,14 +34,16 @@ tools: [read, edit, search, todo, agent] - 필드 누락 시 플레이스홀더로 보정(상태 불명확 -> `draft`, 승인 시각 미기재 -> `{{CONFIRM: 승인 후 기재}}`) - `# 2. 이번 반복 범위 경계`에 In Scope/Out of Scope가 모두 존재하는지 검증 - `# 3. 백로그 작업 분해`에 Item ID, Work Item, 연결 요구사항, 연결 리스크, 담당 컬럼이 존재하는지 검증 - - `# 4. WSJF 점수표`에 BV/TC/RR-OE/Job Size/WSJF/Priority가 존재하는지 검증 - - WSJF 우선순위가 selected_score(WSJF) 내림차순과 일치하는지 검증 + - `# 4. 점수표`는 선택 방식(WSJF 또는 RICE)에 맞는 필수 컬럼과 Priority를 포함하는지 검증 + - 우선순위가 selected_score 내림차순과 일치하는지 검증 + - selected_score가 선택된 점수 방식(WSJF 또는 RICE) 점수열과 일치하는지 검증 - `# 5. 작업별 수용 기준 (AC)`에서 각 작업별 AC가 최소 2개 이상인지 검증 - `# 7. Design 단계 전달 체크`의 체크 근거가 본문과 일치하는지 검증 - `# 8. 파일 처리 결과`의 점수 파일 경로가 docs/sdlc/planning/planning-backlog-scores.out.csv인지 검증 9. CSV 정합성 검증: - docs/sdlc/planning/planning-backlog-scores.out.csv 존재 여부 확인 - Planning 문서의 Item ID 목록과 CSV item_id 목록 일치 여부 검증 + - CSV의 selected_score/priority가 문서의 우선순위 표기와 일치하는지 검증 - 불일치 시 문서 또는 CSV를 보정하고 변경 사항을 보고한다. 10. 수정 규칙: - append 금지, 단일 완성본으로 overwrite 저장 @@ -56,7 +60,7 @@ tools: [read, edit, search, todo, agent] 실행 절차: 1. 입력 ID 목록 정규화 및 형식 검증 -2. 각 ID에 대해 Planning 문서 탐색(1차/2차 규칙 적용) +2. 각 ID에 대해 Planning 문서 탐색(1차/2차 규칙 적용, 2차는 입력 Planning ID 필드 일치 필수) 3. 발견 문서 내용 읽기 4. 재검토 항목 기준으로 누락/불일치 판단 5. 필요한 항목만 보정하여 문서 overwrite 저장 @@ -66,7 +70,7 @@ tools: [read, edit, search, todo, agent] 최종 응답 형식: 1. 실행 입력 요약 -2. 정규화된 대상 Discovery ID 목록 +2. 정규화된 대상 Planning ID 목록 3. 형식 오류 ID 목록(있으면) 4. 미발견 ID 목록(있으면) 5. 문서별 검토 결과 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..852768a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "chat.tools.terminal.autoApprove": { + "git remote": true + } +} \ No newline at end of file From f2466f8544ee2e8d2ed4884daa164bd304f3d361 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Tue, 14 Apr 2026 23:56:21 +0900 Subject: [PATCH 15/21] docs: finalize planning review and confirmation updates --- .github/START-HERE.md | 1 - .github/prompts/planning-review.prompt.md | 28 +++++++++++-------- .../planning/planning-backlog-scores.out.csv | 12 ++++---- ...14_cli-bot-token-lifecycle-cli.planning.md | 27 ++++++++---------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.github/START-HERE.md b/.github/START-HERE.md index 118f035..0e04da7 100644 --- a/.github/START-HERE.md +++ b/.github/START-HERE.md @@ -16,7 +16,6 @@ 2. 파일명 형식: dcy-<3자리문서번호>_YYYY-MM-DD_.discovery.md 3. 날짜는 생성일 기준으로 유지 4. 작성 템플릿: .github/templates/discovery-note.template.md -5. 문서 하단 Discovery Freeze 필수 ## 2. Planning Scoring Quick Start diff --git a/.github/prompts/planning-review.prompt.md b/.github/prompts/planning-review.prompt.md index f01fa02..6ef0cdf 100644 --- a/.github/prompts/planning-review.prompt.md +++ b/.github/prompts/planning-review.prompt.md @@ -1,26 +1,28 @@ --- -description: "Use when: /planning-review to re-review existing planning documents and update them in place from Discovery IDs" +description: "Use when: /planning-review to re-review existing planning documents and update them in place from Planning IDs" name: "planning-review" -argument-hint: "Enter Discovery IDs separated by commas. Example: dcy-001, dcy-002" +argument-hint: "Enter Planning IDs separated by commas. Example: pln-001, pln-002" agent: "agent" model: "reviewer" tools: [read, edit, search, todo, agent] --- 당신은 SDLC Planning 문서 재검토 도우미다. -사용자가 지정한 Discovery 문서 ID 목록을 기준으로 Planning 문서를 재검토하고, 필요한 보정을 반영해 저장한다. +사용자가 지정한 Planning 문서 ID 목록을 기준으로 Planning 문서를 재검토하고, 필요한 보정을 반영해 저장한다. 반드시 아래 규칙을 지켜라. 1. 출력은 한국어로 작성한다. -2. 입력은 쉼표(,) 기준으로 분리해 Discovery ID 목록으로 처리한다. +2. 입력은 쉼표(,) 기준으로 분리해 Planning ID 목록으로 처리한다. 3. 입력 정규화 규칙: - 공백 제거 - - 소문자 정규화(dcy-001 형식) + - 소문자 정규화(pln-001 형식) - 중복 ID 제거 -4. ID 형식은 dcy-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. +4. ID 형식은 pln-<3자리숫자>만 허용한다. 형식 오류 ID는 별도 오류 목록으로 보고한다. 5. Planning 파일 검색 규칙: - - 1차: dcy-001 -> docs/sdlc/planning/pln-001_*.planning.md 패턴 검색 - - 2차: 1차 미발견 시 docs/sdlc/planning/*.planning.md에서 `입력 Discovery:` 줄에 dcy-001 포함 문서 검색 + - 1차: pln-001 -> docs/sdlc/planning/pln-001_*.planning.md 패턴 검색 + - 2차: 1차 미발견 시 docs/sdlc/planning/*.planning.md에서 파일명에 pln-001 토큰이 포함된 문서만 후보로 수집한다. + - 후보 문서는 `입력 Planning ID:` 필드 값이 pln-001과 정확히 일치할 때만 매칭한다. 필드가 없거나 불일치하면 매칭하지 않는다. + - 본문 단순 포함(contains) 검색만으로는 매칭하지 않는다. 6. 존재하지 않는 Planning 문서는 생성하지 않는다. - 미발견 ID는 미발견 목록에 보고하고, 존재 문서만 재검토/수정한다. 7. 점수 산출물 정책: @@ -32,14 +34,16 @@ tools: [read, edit, search, todo, agent] - 필드 누락 시 플레이스홀더로 보정(상태 불명확 -> `draft`, 승인 시각 미기재 -> `{{CONFIRM: 승인 후 기재}}`) - `# 2. 이번 반복 범위 경계`에 In Scope/Out of Scope가 모두 존재하는지 검증 - `# 3. 백로그 작업 분해`에 Item ID, Work Item, 연결 요구사항, 연결 리스크, 담당 컬럼이 존재하는지 검증 - - `# 4. WSJF 점수표`에 BV/TC/RR-OE/Job Size/WSJF/Priority가 존재하는지 검증 - - WSJF 우선순위가 selected_score(WSJF) 내림차순과 일치하는지 검증 + - `# 4. 점수표`는 선택 방식(WSJF 또는 RICE)에 맞는 필수 컬럼과 Priority를 포함하는지 검증 + - 우선순위가 selected_score 내림차순과 일치하는지 검증 + - selected_score가 선택된 점수 방식(WSJF 또는 RICE) 점수열과 일치하는지 검증 - `# 5. 작업별 수용 기준 (AC)`에서 각 작업별 AC가 최소 2개 이상인지 검증 - `# 7. Design 단계 전달 체크`의 체크 근거가 본문과 일치하는지 검증 - `# 8. 파일 처리 결과`의 점수 파일 경로가 docs/sdlc/planning/planning-backlog-scores.out.csv인지 검증 9. CSV 정합성 검증: - docs/sdlc/planning/planning-backlog-scores.out.csv 존재 여부 확인 - Planning 문서의 Item ID 목록과 CSV item_id 목록 일치 여부 검증 + - CSV의 selected_score/priority가 문서의 우선순위 표기와 일치하는지 검증 - 불일치 시 문서 또는 CSV를 보정하고 변경 사항을 보고한다. 10. 수정 규칙: - append 금지, 단일 완성본으로 overwrite 저장 @@ -56,7 +60,7 @@ tools: [read, edit, search, todo, agent] 실행 절차: 1. 입력 ID 목록 정규화 및 형식 검증 -2. 각 ID에 대해 Planning 문서 탐색(1차/2차 규칙 적용) +2. 각 ID에 대해 Planning 문서 탐색(1차/2차 규칙 적용, 2차는 입력 Planning ID 필드 일치 필수) 3. 발견 문서 내용 읽기 4. 재검토 항목 기준으로 누락/불일치 판단 5. 필요한 항목만 보정하여 문서 overwrite 저장 @@ -66,7 +70,7 @@ tools: [read, edit, search, todo, agent] 최종 응답 형식: 1. 실행 입력 요약 -2. 정규화된 대상 Discovery ID 목록 +2. 정규화된 대상 Planning ID 목록 3. 형식 오류 ID 목록(있으면) 4. 미발견 ID 목록(있으면) 5. 문서별 검토 결과 diff --git a/docs/sdlc/planning/planning-backlog-scores.out.csv b/docs/sdlc/planning/planning-backlog-scores.out.csv index ebd4641..5037eda 100644 --- a/docs/sdlc/planning/planning-backlog-scores.out.csv +++ b/docs/sdlc/planning/planning-backlog-scores.out.csv @@ -1,6 +1,6 @@ -item_id,work_item,business_value,time_criticality,rr_oe,job_size,reach,impact,confidence,effort,method,notes -P-002,만기 정책 및 권한 가드 구현 (--no-expiry 및 기본 TTL 365일),9,10,9,4,,,,,WSJF,"WSJF=7.00; Priority=1; FR-3/R-1" -P-003,토큰 출력 마스킹 및 감사 로그 구현,8,8,10,4,,,,,WSJF,"WSJF=6.50; Priority=2; NFR-1/NFR-2/R-3" -P-001,CLI bot 토큰 발급/삭제 명령 추가,10,9,8,5,,,,,WSJF,"WSJF=5.40; Priority=3; FR-1/FR-2/FR-4/FR-5" -P-004,CLI UX 정합성 보강 (confirm-force/오류/출력 포맷),7,7,7,4,,,,,WSJF,"WSJF=5.25; Priority=4; NFR-3/NFR-4" -P-005,테스트 및 문서 동기화,7,6,8,5,,,,,WSJF,"WSJF=4.20; Priority=5; S-1/S-2/S-3" +item_id,work_item,business_value,time_criticality,rr_oe,job_size,reach,impact,confidence,effort,method,selected_score,priority,notes +P-002,만기 정책 및 권한 가드 구현 (--no-expiry 및 기본 TTL 365일),9,10,9,4,,,,,WSJF,7.00,1,"FR-3/R-1" +P-003,토큰 출력 마스킹 및 감사 로그 구현,8,8,10,4,,,,,WSJF,6.50,2,"NFR-1/NFR-2/R-3" +P-001,CLI bot 토큰 발급/삭제 명령 추가,10,9,8,5,,,,,WSJF,5.40,3,"FR-1/FR-2/FR-4/FR-5" +P-004,CLI UX 정합성 보강 (confirm-force/오류/출력 포맷),7,7,7,4,,,,,WSJF,5.25,4,"NFR-3/NFR-4" +P-005,테스트 및 문서 동기화,7,6,8,5,,,,,WSJF,4.20,5,"S-1/S-2/S-3" diff --git a/docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md b/docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md index f732f11..c5eeefe 100644 --- a/docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md +++ b/docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md @@ -1,7 +1,7 @@ # 0. 문서 상태 -- 상태: draft +- 상태: confirmed - 승인자: Justin -- 승인 시각: {{CONFIRM: 승인 후 기재}} +- 승인 시각: 2026-04-14 # 1. Planning 개요 - 입력 Discovery: dcy-001 @@ -35,14 +35,14 @@ | P-004 | CLI UX 정합성 보강(확인 프롬프트, `--force`, 오류 메시지) | FR-4, NFR-3, NFR-4 | R-2 | Builder | | P-005 | 테스트/문서 동기화(README, API/CLI 계약) | S-1, S-2, S-3 | R-2, R-3 | Builder | -# 4. WSJF 점수표 -| Item ID | Work Item | Business Value | Time Criticality | RR/OE | Job Size | WSJF Score | Priority | -|---|---:|---:|---:|---:|---:|---:|---:| -| P-002 | 만기 정책 및 권한 가드 구현 | 9 | 10 | 9 | 4 | 7.00 | 1 | -| P-003 | 토큰 출력 마스킹 및 감사 로그 구현 | 8 | 8 | 10 | 4 | 6.50 | 2 | -| P-001 | CLI bot 토큰 발급/삭제 명령 추가 | 10 | 9 | 8 | 5 | 5.40 | 3 | -| P-004 | CLI UX 정합성 보강 | 7 | 7 | 7 | 4 | 5.25 | 4 | -| P-005 | 테스트/문서 동기화 | 7 | 6 | 8 | 5 | 4.20 | 5 | +# 4. 점수표 (WSJF) +| Item ID | Work Item | Business Value | Time Criticality | RR/OE | Job Size | WSJF Score | selected_score | Priority | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| P-002 | 만기 정책 및 권한 가드 구현 | 9 | 10 | 9 | 4 | 7.00 | 7.00 | 1 | +| P-003 | 토큰 출력 마스킹 및 감사 로그 구현 | 8 | 8 | 10 | 4 | 6.50 | 6.50 | 2 | +| P-001 | CLI bot 토큰 발급/삭제 명령 추가 | 10 | 9 | 8 | 5 | 5.40 | 5.40 | 3 | +| P-004 | CLI UX 정합성 보강 | 7 | 7 | 7 | 4 | 5.25 | 5.25 | 4 | +| P-005 | 테스트/문서 동기화 | 7 | 6 | 8 | 5 | 4.20 | 4.20 | 5 | 계산식: WSJF = (Business Value + Time Criticality + RR/OE) / Job Size @@ -88,14 +88,11 @@ - [x] 우선순위 상위 백로그의 AC가 검증 가능한 문장으로 정의됨 - [x] WSJF 계산 근거 기록 완료 - [x] 이번 반복 In Scope/Out of Scope 확정 -- [ ] 최종 승인 - - {{CONFIRM: Planning 초안 승인 여부를 확인하라}} +- [x] 최종 승인 # 8. 파일 처리 결과 - 생성/갱신 경로: docs/sdlc/planning/pln-001_2026-04-14_cli-bot-token-lifecycle-cli.planning.md - 점수 파일 갱신: docs/sdlc/planning/planning-backlog-scores.out.csv # 9. 사용자 결정 필요 항목 요약 -## {{CONFIRM}} -- 승인 시각 기재. (위치: #0) -- Planning 초안 승인 여부를 확인하라. (위치: #7) +현재 미해결 플레이스홀더 없음 From f993cebf01e04f06d1665b9a8c13a3906491ff64 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Wed, 15 Apr 2026 00:06:50 +0900 Subject: [PATCH 16/21] docs: confirm design draft dsn-001 --- ...4-14_cli-bot-token-lifecycle-cli.design.md | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 docs/sdlc/design/dsn-001_2026-04-14_cli-bot-token-lifecycle-cli.design.md diff --git a/docs/sdlc/design/dsn-001_2026-04-14_cli-bot-token-lifecycle-cli.design.md b/docs/sdlc/design/dsn-001_2026-04-14_cli-bot-token-lifecycle-cli.design.md new file mode 100644 index 0000000..339954e --- /dev/null +++ b/docs/sdlc/design/dsn-001_2026-04-14_cli-bot-token-lifecycle-cli.design.md @@ -0,0 +1,171 @@ +# 0. 문서 상태 +- 상태: confirmed +- 승인자: Justin +- 승인 시각: 2026-04-15 + +# 1. Design 개요 +- 입력 Planning: pln-001 +- 목적: CLI bot 토큰 발급/삭제 기능을 정책 일관성, 보안성, 운영성 기준으로 구현 가능하도록 인터페이스/흐름/검증 포인트를 확정한다. +- 설계 대상 작업 (WSJF 상위 3개): + - 1순위: P-002 만기 정책 및 권한 가드 구현 + - 2순위: P-003 토큰 출력 마스킹 및 감사 로그 구현 + - 3순위: P-001 CLI bot 토큰 발급/삭제 명령 추가 + +# 2. 설계 대안 비교 (우선순위 상위 작업) +## P-002: 만기 정책 및 권한 가드 구현 +### 대안 A +- 설명: CLI 입력 단계(Typer 옵션 파싱/검증)에서 TTL 기본값, `--no-expiry` 상호배타성, 관리자 정책 검증을 선처리한다. +- 장점: + - 잘못된 입력을 서비스 호출 전에 차단해 오류 원인이 명확하다. + - 단위 테스트가 간단하며 사용자 피드백이 빠르다. +- 단점: + - 정책 검증 로직이 CLI 계층에 치우치면 API 계층과 중복될 수 있다. + +### 대안 B +- 설명: 정책 검증을 auth/service 계층에서 처리하고 CLI는 입력 전달만 수행한다. +- 장점: + - 정책 소스가 단일화되어 API/CLI 일관성이 높다. + - 다른 채널(예: 배치, API)에서도 동일 정책 재사용이 쉽다. +- 단점: + - CLI 사용자 입장에서는 오류 맥락이 늦게 반환될 수 있다. + - 서비스 에러를 CLI 친화 메시지로 재매핑하는 추가 코드가 필요하다. + +### 선택 결정 +- 선택: 대안 A+B 혼합 +- 근거: 상호배타성/입력 형식 검증은 CLI에서 조기 차단하고, 권한 및 최종 만기 정책 강제는 서비스 계층에서 재검증해 정책 일관성과 UX를 동시에 확보한다. + +## P-003: 토큰 출력 마스킹 및 감사 로그 구현 +### 대안 A +- 설명: 발급 응답 DTO 생성 시점에서 마스킹된 값만 남기고 원문을 폐기한다. +- 장점: + - 출력/로그 경로를 일괄 통제해 원문 노출 위험이 낮다. + - 후속 렌더링 계층(table/json)에서 추가 안전장치 부담이 줄어든다. +- 단점: + - 발급 직후 1회 원문 출력 요구를 처리하기 위해 예외 흐름이 필요하다. + +### 대안 B +- 설명: 출력 직전 렌더링 함수에서 마스킹을 적용하고 내부 모델에는 원문을 유지한다. +- 장점: + - 기존 데이터 모델 변경이 적다. + - 일부 디버깅 시 내부 상태 확인이 쉽다. +- 단점: + - 로그/예외 경로에서 원문 누출 가능성이 높다. + - 마스킹 누락 지점이 생기기 쉽다. + +### 선택 결정 +- 선택: 대안 A +- 근거: NFR-1/NFR-2와 R-3 완화 우선순위가 높아, 원문 보존 경로를 최소화하는 구조가 안전하다. + +## P-001: CLI bot 토큰 발급/삭제 명령 추가 +### 대안 A +- 설명: 기존 `user/portfolio/strategy`와 동일하게 `bot` Typer group을 추가하고 `token issue`, `token revoke` 서브커맨드로 구성한다. +- 장점: + - 현재 CLI 구조와 일관되어 학습 비용이 낮다. + - 명령 확장(예: token list) 시 구조가 자연스럽다. +- 단점: + - 초기 구현 시 커맨드 트리/도움말 정비가 필요하다. + +### 대안 B +- 설명: 기존 `user` 그룹에 `user bot-token issue/revoke` 형태로 통합한다. +- 장점: + - 새로운 최상위 그룹 추가가 없어 변경 범위가 작다. +- 단점: + - 사용자 관리와 bot 토큰 수명주기가 섞여 명령 의미가 불명확해진다. + +### 선택 결정 +- 선택: 대안 A +- 근거: P-004의 UX 일관성 요구를 충족하고 장기 확장성(봇/토큰 명령 확장)에 유리하다. + +# 3. 인터페이스 정의 +## P-002: 만기 정책 및 권한 가드 구현 +- 입력: `--ttl-days`(optional), `--no-expiry`(flag), 실행자 컨텍스트(admin 여부) +- 출력: 정책 유효성 통과 시 정규화된 만기 정책 객체(`expires_at` 또는 `never_expires=true`) +- 오류 경로: 옵션 상충, 음수/0 TTL, 비관리자 `--no-expiry` 사용 시 검증 오류 반환 + +## P-003: 토큰 출력 마스킹 및 감사 로그 구현 +- 입력: 발급/삭제 결과 객체(`token_id`, `raw_token` optional, `actor_id`, `bot_id`, `timestamp`) +- 출력: 사용자 출력용 마스킹 토큰 값, 감사 로그 이벤트 레코드 +- 오류 경로: 감사 로그 기록 실패 시 `issue`는 트랜잭션 롤백으로 발급을 취소하고 명령을 실패 처리한다. `revoke`는 보안 우선 원칙에 따라 폐기는 유지하고 명령은 실패 처리하며 운영 경고를 발생시킨다. + +## P-001: CLI bot 토큰 발급/삭제 명령 추가 +- 입력: `bot_id`, 발급 옵션(`--ttl-days`/`--no-expiry`), 공통 옵션(`--force`, `--format`) +- 출력: issue 시 `token_id/expires_at/revoked(false)` + 1회 토큰 원문, revoke 시 `token_id/revoked(true)` +- 오류 경로: 미존재 bot/token, 권한 부족, 정책 위반, 저장소/서비스 예외 + +## P-004: CLI UX 정합성 보강 +- 입력: 커맨드 실행 컨텍스트(확인 프롬프트 응답, `--force`, 출력 모드) +- 출력: 표준화된 성공/실패 출력(table/json) +- 오류 경로: 포맷 미지원, 사용자 취소 시 정상 종료(code 0), 도메인 오류 시 비정상 종료(code != 0) + +## P-005: 테스트/문서 동기화 +- 입력: 테스트 케이스 정의, docs sync 규칙 대상 파일 목록 +- 출력: 테스트 결과 리포트, 업데이트된 README/API 계약 문서 +- 오류 경로: 테스트 실패, docs sync 체크 실패, 누락 문서 갱신 감지 + +# 4. 데이터/흐름 설계 +## P-002: 만기 정책 및 권한 가드 구현 +- 주요 데이터 경로: + - CLI 옵션 파싱 -> 입력 검증 -> 서비스 계층 정책 재검증 -> repository 요청 페이로드 구성 +- 상태 변화: + - `draft_policy` -> `validated_policy` -> `persistable_policy` + +## P-003: 토큰 출력 마스킹 및 감사 로그 구현 +- 주요 데이터 경로: + - 서비스 발급/삭제 결과 -> 마스킹 변환 -> 출력 렌더링 + 감사 로그 sink 기록 +- 상태 변화: + - `raw_token_state` -> `masked_view_state` -> `audited_state` + +## P-001: CLI bot 토큰 발급/삭제 명령 추가 +- 주요 데이터 경로: + - command dispatch -> service 호출(issue/revoke) -> 결과 DTO -> renderer(table/json) +- 상태 변화: + - issue: `requested` -> `issued(active)` + - revoke: `issued(active)` -> `revoked(inactive)` + +## P-004: CLI UX 정합성 보강 +- 주요 데이터 경로: + - confirmation gate -> command executor -> unified output/error handler +- 상태 변화: + - `pending_confirmation` -> `executed` 또는 `cancelled` + +## P-005: 테스트/문서 동기화 +- 주요 데이터 경로: + - code change set -> test run -> docs sync check -> verification evidence bundle +- 상태 변화: + - `unverified` -> `verified` 또는 `failed-with-evidence` + +# 5. 테스트 전략 연계 (AC 매핑) +| 작업 ID | AC | 검증 방법 | 검증 포인트 | +|---|---|---|---| +| P-002 | AC-1 | CLI 통합 테스트 | `ttl-days` 미입력 시 `expires_at`이 현재+365일로 계산됨 | +| P-002 | AC-2 | 권한 예외 테스트 | 비관리자 컨텍스트에서 `--no-expiry` 요청 시 오류 코드/메시지 확인 | +| P-002 | AC-3 | 옵션 조합 검증 테스트 | `--ttl-days`와 `--no-expiry` 동시 지정 시 검증 실패 | +| P-003 | AC-1 | 출력 스냅샷 테스트 | issue 이후 재조회/목록 출력에서 앞6/뒤4 외 마스킹 확인 | +| P-003 | AC-2 | 감사 로그 검증 테스트 | issue/revoke 각각에 actor/bot/token_id(마스킹)/결과 로그 생성 확인 | +| P-003 | AC-3 | 보안 회귀 테스트 | 로그 파일/콘솔에 raw token 미노출 확인 | +| P-001 | AC-1 | 커맨드 help 테스트 | `bot token issue`, `bot token revoke` help 노출 확인 | +| P-001 | AC-2 | 성공 경로 E2E | issue 응답 필수 필드(`token_id/expires_at/revoked`) 존재 확인 | +| P-001 | AC-3 | revoke E2E | revoke 이후 동일 토큰 인증 실패 확인 | +| P-004 | AC-1 | UX 동작 테스트 | 기본 프롬프트 표시, `--force` 시 생략 확인 | +| P-004 | AC-2 | 예외 메시지 테스트 | 잘못된 bot/token 입력에서 사용자 지침 포함 메시지 확인 | +| P-005 | AC-1/AC-2 | CI 파이프라인 검증 | 정상+예외 테스트 최소 1건씩 통과 및 docs sync 통과 | + +# 6. 리스크 대응 설계 +| 리스크 ID | 리스크 내용 | 설계 대응 전략 | +|---|---|---| +| R-1 | 만기 정책 모호성으로 무기한 토큰 남발 가능 | CLI+서비스 이중 검증, 기본 TTL 365일 강제, `--no-expiry` 제한 경로 및 감사 로그 필수화 | +| R-2 | 명령 체계 확장 시 오사용 가능 | `bot` 전용 그룹으로 분리, help 명세 표준화, 오류 메시지에 다음 행동 가이드 포함 | +| R-3 | 토큰 출력/로그 유출 위험 | 원문 1회 출력 후 즉시 마스킹 전환, 로그 저장 시 원문 금지, 보안 회귀 테스트 추가 | + +# 7. Implementation 단계 전달 체크 +- [x] 우선순위 상위 작업의 설계 근거(대안 비교 + 선택 이유)가 존재함 +- [x] 전체 작업의 인터페이스(입력/출력/오류 경로)가 정의됨 +- [x] AC와 검증 포인트가 매핑됨 +- [x] Planning 리스크에 대한 설계 대응이 반영됨 +- [x] 최종 승인 + +# 8. 파일 처리 결과 +- 생성/갱신 경로: docs/sdlc/design/dsn-001_2026-04-14_cli-bot-token-lifecycle-cli.design.md + +# 9. 사용자 결정 필요 항목 요약 +현재 미해결 플레이스홀더 없음 From 0abb3753eca60157e1e1b8fe7f80404237e3628c Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Wed, 15 Apr 2026 00:29:21 +0900 Subject: [PATCH 17/21] Squashed '.vendor/stage-pilot/' changes from 575a73c..28ff1bd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 28ff1bd chore(prompt): commit.prompt.md에 execute 툴 옵션 추가 17b0e7a hore(prompt): GPT-4.1 기반 자동 커밋/푸시 지원 commit.prompt.md 추가 08e6291 chore: refine prompt rules for design review and implementation 7521d73 draft c87b134 draft git-subtree-dir: .vendor/stage-pilot git-subtree-split: 28ff1bd30b0151e97641575c10930b2ac33dfc38 --- .github/prompts/commit.prompt.md | 54 ++++++++++++++++++++++++ .github/prompts/design-review.prompt.md | 6 ++- .github/prompts/implementation.prompt.md | 16 ++++--- .gitignore | 1 + .vscode/settings.json | 5 --- 5 files changed, 70 insertions(+), 12 deletions(-) create mode 100644 .github/prompts/commit.prompt.md create mode 100644 .gitignore delete mode 100644 .vscode/settings.json diff --git a/.github/prompts/commit.prompt.md b/.github/prompts/commit.prompt.md new file mode 100644 index 0000000..8863c5e --- /dev/null +++ b/.github/prompts/commit.prompt.md @@ -0,0 +1,54 @@ +--- +description: "Use when: /commit or /commit all to review changes, generate a commit message, commit, and optionally push with --push" +name: "commit" +argument-hint: "Optional: 'all' and/or '--push'. Examples: '/commit', '/commit all', '/commit --push', '/commit all --push'" +agent: "agent" +model: "GPT-4.1" +tools: [read, search, agent, todo, execute] +--- + +당신은 변경사항을 검토하고 커밋/푸시를 자동으로 수행하는 실행 도우미다. +반드시 GPT-4.1 모델 기준으로 동작한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 해석 규칙: + - 빈 입력 또는 `all`: `/commit`과 동일하게 처리한다. + - `--push` 옵션이 있으면 커밋 후 원격 푸시까지 수행한다. + - 허용 예시: `/commit`, `/commit all`, `/commit --push`, `/commit all --push` + - 위 형식 외 인자는 오류로 보고하고 종료한다. +3. 실행 전 검증: + - git 저장소가 아니면 오류로 보고하고 종료한다. + - 변경 파일이 없으면 "커밋할 변경사항이 없다"고 보고하고 종료한다. +4. 변경 검토: + - `git status --short`와 `git diff`(필요 시 `git diff --staged`)로 변경을 파악한다. + - 변경 의도를 요약한 커밋 메시지를 1줄로 생성한다. + - 커밋 메시지 형식은 Conventional Commits를 우선 사용한다. +5. 커밋 수행: + - `/commit`, `/commit all` 모두 `git add -A` 후 `git commit -m "<생성 메시지>"`를 수행한다. + - 커밋 실패 시 오류 내용을 보고하고 종료한다. +6. 푸시 수행: + - `--push` 옵션이 있을 때만 `git push`를 수행한다. + - 기본 대상은 현재 체크아웃 브랜치의 upstream을 사용한다. + - upstream이 없으면 `origin <현재브랜치>`로 푸시를 시도하고 결과를 보고한다. +7. 안전 규칙: + - 파괴적 명령(`reset --hard`, `checkout --`, 강제 push)은 사용하지 않는다. + - 사용자가 명시하지 않은 변경을 되돌리지 않는다. + +실행 절차: +1. 입력 옵션 파싱(`all`, `--push`) +2. 저장소/변경 여부 확인 +3. 변경 내용 검토 및 커밋 메시지 생성 +4. `git add -A` + `git commit` +5. 옵션이 있으면 `git push` +6. 결과 보고 + +최종 응답 형식: +1. 실행 옵션 요약 +2. 생성된 커밋 메시지 +3. 커밋 결과(해시, 변경 파일 수) +4. 푸시 결과(실행한 경우) +5. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/design-review.prompt.md b/.github/prompts/design-review.prompt.md index 4416144..d36c046 100644 --- a/.github/prompts/design-review.prompt.md +++ b/.github/prompts/design-review.prompt.md @@ -48,11 +48,15 @@ tools: [read, edit, search, todo, agent] - {{DECIDE: 질문}} - {{CONFIRM: 내용}} - {{DATA: 필요한 정보}} + - {{DELEGATE_TO: agent}} (agent 자동 결정) - 추가 의사결정이 필요한 경우 반드시 플레이스홀더를 삽입한다. 11. 플레이스홀더 집계 규칙: - 본문에 미해결 플레이스홀더가 있으면 `# 9. 사용자 결정 필요 항목 요약`에 유형별로 집계한다. + - 집계 시 {{DECIDE}}, {{CONFIRM}}, {{DATA}}, {{DELEGATE_TO}}를 유형별로 분리하고 중복 문구는 1회만 남긴다. - 본문에 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. 12. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. +13. 본문 플레이스홀더 정규화 규칙: + - {{DELEGATE_TO: agent}} 형식의 항목은 agent가 결정을 생성해 본문 문장으로 대체하고, 요약 섹션에서도 제거한다. 실행 절차: 1. 입력 ID 목록 정규화 및 형식 검증 @@ -72,7 +76,7 @@ tools: [read, edit, search, todo, agent] 5. 연결 Planning 미승인/미발견 목록(있으면) 6. 문서별 검토 결과 7. 문서별 수정 내역 요약 -8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}/{{DELEGATE_TO}}) 9. 업데이트된 파일 경로 목록 10. 다음 액션 diff --git a/.github/prompts/implementation.prompt.md b/.github/prompts/implementation.prompt.md index 25062bb..8922208 100644 --- a/.github/prompts/implementation.prompt.md +++ b/.github/prompts/implementation.prompt.md @@ -12,9 +12,10 @@ tools: [read, edit, search, create_file, agent, todo] 반드시 아래 규칙을 지켜라. 1. 출력은 한국어로 작성한다. -2. 입력 형식: `imp-<3자리숫자> <항목목록>` +2. 입력 형식: `imp-<3자리숫자>` 또는 `imp-<3자리숫자> <항목목록>` - imp ID: `imp-<3자리숫자>` 형식 필수 - - 항목 목록: `I-001, I-002` 또는 `P-001, P-002` 또는 혼합 가능 + - 항목 목록(선택): `I-001, I-002` 또는 `P-001, P-002` 또는 혼합 가능 + - 항목 목록이 생략되면 imp 문서 `# 3. 작업 단위 분해`의 미완료 항목 전체를 위에서 아래 순서대로 실행한다. - 형식 오류 시 오류로 보고하고 종료한다. 3. imp 파일 검색 규칙: - 경로: docs/sdlc/implementation/ @@ -23,6 +24,8 @@ tools: [read, edit, search, create_file, agent, todo] 4. imp 문서 상태 확인: - 상태가 `draft`가 아니면 "이미 확정된 Implementation 문서다. 상태를 확인하라"로 보고하고 종료한다. 5. 항목 해석 규칙: + - 항목 목록이 없는 경우: `# 3. 작업 단위 분해` 표의 모든 I-ID 중 상태가 `완료`가 아닌 항목을 문서 순서대로 대상에 포함한다. + - 항목 목록이 없는데 미완료 I-ID가 없으면 "실행할 미완료 항목이 없다"고 보고하고 종료한다. - I-ID 입력 시: imp 문서 `# 3. 작업 단위 분해` 표에서 해당 I-ID 행을 직접 찾는다. - P-ID 입력 시: `# 3. 작업 단위 분해` 표에서 `연결 설계/AC` 열에 해당 P-ID가 포함된 모든 I-ID를 찾는다. - 상태가 이미 `완료`인 항목은 건너뛰고 보고한다. @@ -46,14 +49,15 @@ tools: [read, edit, search, create_file, agent, todo] - 확인 필요: {{CONFIRM: 내용}} - 추가 정보 필요: {{DATA: 필요한 정보}} 10. 범위 제한: - - 이번 명령에서 지정하지 않은 I-ID는 절대 수정하지 않는다. + - 항목 목록을 명시한 경우, 이번 명령에서 지정하지 않은 I-ID는 절대 수정하지 않는다. + - 항목 목록이 생략된 경우, 문서의 미완료 I-ID 전체가 이번 명령의 지정 항목으로 간주된다. - imp 문서 외 SDLC 문서(Design, Planning 등)는 참조만 하고 수정하지 않는다. - 지정한 파일 변경 계획 외의 추가 파일을 임의로 수정하지 않는다. 실행 절차: -1. 입력 파싱: imp ID + 항목 목록 형식 검증 +1. 입력 파싱: imp ID 및 항목 목록(선택) 형식 검증 2. imp 파일 검색 및 상태 확인 -3. 항목 해석: I-ID 직접 조회 또는 P-ID → I-ID 변환 +3. 항목 해석: 항목 목록 없음 -> 미완료 I-ID 전체(문서 순서), 항목 목록 있음 -> I-ID 직접 조회 또는 P-ID → I-ID 변환 4. 상태가 이미 `완료`인 항목 필터링 및 보고 5. Design 문서 읽기 (실행 내용 근거 확보) 6. 항목별 순서대로 실행: @@ -64,7 +68,7 @@ tools: [read, edit, search, create_file, agent, todo] 8. 최종 응답 제공 최종 응답 형식: -1. 실행 입력 요약 (imp ID, 지정 항목 목록) +1. 실행 입력 요약 (imp ID, 지정 항목 목록 또는 전체 미완료 자동 선택) 2. 항목별 실행 결과 (성공/건너뜀/실패) 3. 생성/수정된 실제 파일 경로 목록 4. imp 문서 갱신 내역 (상태 변경된 항목) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbe9c82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 852768a..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "chat.tools.terminal.autoApprove": { - "git remote": true - } -} \ No newline at end of file From 0e9d2a16d81e6c7c2469e2e0a09886d690730414 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Wed, 15 Apr 2026 00:48:22 +0900 Subject: [PATCH 18/21] =?UTF-8?q?feat:=20=EC=A3=BC=EC=9A=94=20CLI,=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D,=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B0=8F=20=EB=AC=B8=EC=84=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/prompts/commit.prompt.md | 54 +++++++ .github/prompts/design-review.prompt.md | 6 +- .github/prompts/implementation.prompt.md | 16 +- README.md | 12 ++ app/cli/commands/bot_tokens.py | 115 +++++++++++++ app/cli/core/output.py | 30 ++++ app/cli/main.py | 2 + app/core/security.py | 39 ++++- app/domain/services.py | 7 + app/repositories/postgres.py | 59 +++++++ app/schemas/auth.py | 18 ++- app/services/auth_service.py | 85 +++++++++- ...-bot-token-lifecycle-cli.implementation.md | 109 +++++++++++++ docs/technical/auth-and-token-lifecycle.md | 20 +++ tests/test_app.py | 15 ++ tests/test_auth.py | 142 ++++++++++++++++ tests/test_cli.py | 153 +++++++++++++++++- 17 files changed, 864 insertions(+), 18 deletions(-) create mode 100644 .github/prompts/commit.prompt.md create mode 100644 app/cli/commands/bot_tokens.py create mode 100644 app/cli/core/output.py create mode 100644 docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md diff --git a/.github/prompts/commit.prompt.md b/.github/prompts/commit.prompt.md new file mode 100644 index 0000000..8863c5e --- /dev/null +++ b/.github/prompts/commit.prompt.md @@ -0,0 +1,54 @@ +--- +description: "Use when: /commit or /commit all to review changes, generate a commit message, commit, and optionally push with --push" +name: "commit" +argument-hint: "Optional: 'all' and/or '--push'. Examples: '/commit', '/commit all', '/commit --push', '/commit all --push'" +agent: "agent" +model: "GPT-4.1" +tools: [read, search, agent, todo, execute] +--- + +당신은 변경사항을 검토하고 커밋/푸시를 자동으로 수행하는 실행 도우미다. +반드시 GPT-4.1 모델 기준으로 동작한다. + +반드시 아래 규칙을 지켜라. +1. 출력은 한국어로 작성한다. +2. 입력 해석 규칙: + - 빈 입력 또는 `all`: `/commit`과 동일하게 처리한다. + - `--push` 옵션이 있으면 커밋 후 원격 푸시까지 수행한다. + - 허용 예시: `/commit`, `/commit all`, `/commit --push`, `/commit all --push` + - 위 형식 외 인자는 오류로 보고하고 종료한다. +3. 실행 전 검증: + - git 저장소가 아니면 오류로 보고하고 종료한다. + - 변경 파일이 없으면 "커밋할 변경사항이 없다"고 보고하고 종료한다. +4. 변경 검토: + - `git status --short`와 `git diff`(필요 시 `git diff --staged`)로 변경을 파악한다. + - 변경 의도를 요약한 커밋 메시지를 1줄로 생성한다. + - 커밋 메시지 형식은 Conventional Commits를 우선 사용한다. +5. 커밋 수행: + - `/commit`, `/commit all` 모두 `git add -A` 후 `git commit -m "<생성 메시지>"`를 수행한다. + - 커밋 실패 시 오류 내용을 보고하고 종료한다. +6. 푸시 수행: + - `--push` 옵션이 있을 때만 `git push`를 수행한다. + - 기본 대상은 현재 체크아웃 브랜치의 upstream을 사용한다. + - upstream이 없으면 `origin <현재브랜치>`로 푸시를 시도하고 결과를 보고한다. +7. 안전 규칙: + - 파괴적 명령(`reset --hard`, `checkout --`, 강제 push)은 사용하지 않는다. + - 사용자가 명시하지 않은 변경을 되돌리지 않는다. + +실행 절차: +1. 입력 옵션 파싱(`all`, `--push`) +2. 저장소/변경 여부 확인 +3. 변경 내용 검토 및 커밋 메시지 생성 +4. `git add -A` + `git commit` +5. 옵션이 있으면 `git push` +6. 결과 보고 + +최종 응답 형식: +1. 실행 옵션 요약 +2. 생성된 커밋 메시지 +3. 커밋 결과(해시, 변경 파일 수) +4. 푸시 결과(실행한 경우) +5. 다음 액션 + +사용자 입력: +{{input}} diff --git a/.github/prompts/design-review.prompt.md b/.github/prompts/design-review.prompt.md index 4416144..d36c046 100644 --- a/.github/prompts/design-review.prompt.md +++ b/.github/prompts/design-review.prompt.md @@ -48,11 +48,15 @@ tools: [read, edit, search, todo, agent] - {{DECIDE: 질문}} - {{CONFIRM: 내용}} - {{DATA: 필요한 정보}} + - {{DELEGATE_TO: agent}} (agent 자동 결정) - 추가 의사결정이 필요한 경우 반드시 플레이스홀더를 삽입한다. 11. 플레이스홀더 집계 규칙: - 본문에 미해결 플레이스홀더가 있으면 `# 9. 사용자 결정 필요 항목 요약`에 유형별로 집계한다. + - 집계 시 {{DECIDE}}, {{CONFIRM}}, {{DATA}}, {{DELEGATE_TO}}를 유형별로 분리하고 중복 문구는 1회만 남긴다. - 본문에 미해결 플레이스홀더가 없으면 `현재 미해결 플레이스홀더 없음`으로 정규화한다. 12. 여러 문서를 처리할 때 문서별 결과를 분리해 보고한다. +13. 본문 플레이스홀더 정규화 규칙: + - {{DELEGATE_TO: agent}} 형식의 항목은 agent가 결정을 생성해 본문 문장으로 대체하고, 요약 섹션에서도 제거한다. 실행 절차: 1. 입력 ID 목록 정규화 및 형식 검증 @@ -72,7 +76,7 @@ tools: [read, edit, search, todo, agent] 5. 연결 Planning 미승인/미발견 목록(있으면) 6. 문서별 검토 결과 7. 문서별 수정 내역 요약 -8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}) +8. 문서별 추가된 사용자 결정 항목({{DECIDE}}/{{CONFIRM}}/{{DATA}}/{{DELEGATE_TO}}) 9. 업데이트된 파일 경로 목록 10. 다음 액션 diff --git a/.github/prompts/implementation.prompt.md b/.github/prompts/implementation.prompt.md index 25062bb..8922208 100644 --- a/.github/prompts/implementation.prompt.md +++ b/.github/prompts/implementation.prompt.md @@ -12,9 +12,10 @@ tools: [read, edit, search, create_file, agent, todo] 반드시 아래 규칙을 지켜라. 1. 출력은 한국어로 작성한다. -2. 입력 형식: `imp-<3자리숫자> <항목목록>` +2. 입력 형식: `imp-<3자리숫자>` 또는 `imp-<3자리숫자> <항목목록>` - imp ID: `imp-<3자리숫자>` 형식 필수 - - 항목 목록: `I-001, I-002` 또는 `P-001, P-002` 또는 혼합 가능 + - 항목 목록(선택): `I-001, I-002` 또는 `P-001, P-002` 또는 혼합 가능 + - 항목 목록이 생략되면 imp 문서 `# 3. 작업 단위 분해`의 미완료 항목 전체를 위에서 아래 순서대로 실행한다. - 형식 오류 시 오류로 보고하고 종료한다. 3. imp 파일 검색 규칙: - 경로: docs/sdlc/implementation/ @@ -23,6 +24,8 @@ tools: [read, edit, search, create_file, agent, todo] 4. imp 문서 상태 확인: - 상태가 `draft`가 아니면 "이미 확정된 Implementation 문서다. 상태를 확인하라"로 보고하고 종료한다. 5. 항목 해석 규칙: + - 항목 목록이 없는 경우: `# 3. 작업 단위 분해` 표의 모든 I-ID 중 상태가 `완료`가 아닌 항목을 문서 순서대로 대상에 포함한다. + - 항목 목록이 없는데 미완료 I-ID가 없으면 "실행할 미완료 항목이 없다"고 보고하고 종료한다. - I-ID 입력 시: imp 문서 `# 3. 작업 단위 분해` 표에서 해당 I-ID 행을 직접 찾는다. - P-ID 입력 시: `# 3. 작업 단위 분해` 표에서 `연결 설계/AC` 열에 해당 P-ID가 포함된 모든 I-ID를 찾는다. - 상태가 이미 `완료`인 항목은 건너뛰고 보고한다. @@ -46,14 +49,15 @@ tools: [read, edit, search, create_file, agent, todo] - 확인 필요: {{CONFIRM: 내용}} - 추가 정보 필요: {{DATA: 필요한 정보}} 10. 범위 제한: - - 이번 명령에서 지정하지 않은 I-ID는 절대 수정하지 않는다. + - 항목 목록을 명시한 경우, 이번 명령에서 지정하지 않은 I-ID는 절대 수정하지 않는다. + - 항목 목록이 생략된 경우, 문서의 미완료 I-ID 전체가 이번 명령의 지정 항목으로 간주된다. - imp 문서 외 SDLC 문서(Design, Planning 등)는 참조만 하고 수정하지 않는다. - 지정한 파일 변경 계획 외의 추가 파일을 임의로 수정하지 않는다. 실행 절차: -1. 입력 파싱: imp ID + 항목 목록 형식 검증 +1. 입력 파싱: imp ID 및 항목 목록(선택) 형식 검증 2. imp 파일 검색 및 상태 확인 -3. 항목 해석: I-ID 직접 조회 또는 P-ID → I-ID 변환 +3. 항목 해석: 항목 목록 없음 -> 미완료 I-ID 전체(문서 순서), 항목 목록 있음 -> I-ID 직접 조회 또는 P-ID → I-ID 변환 4. 상태가 이미 `완료`인 항목 필터링 및 보고 5. Design 문서 읽기 (실행 내용 근거 확보) 6. 항목별 순서대로 실행: @@ -64,7 +68,7 @@ tools: [read, edit, search, create_file, agent, todo] 8. 최종 응답 제공 최종 응답 형식: -1. 실행 입력 요약 (imp ID, 지정 항목 목록) +1. 실행 입력 요약 (imp ID, 지정 항목 목록 또는 전체 미완료 자동 선택) 2. 항목별 실행 결과 (성공/건너뜀/실패) 3. 생성/수정된 실제 파일 경로 목록 4. imp 문서 갱신 내역 (상태 변경된 항목) diff --git a/README.md b/README.md index f1f4bab..88c8c54 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,9 @@ portfoliohub user create --username alice --password password123 --email alice@e portfoliohub user get portfoliohub user update --username alice-updated --force portfoliohub user delete --force +portfoliohub bot token issue --name nightly --ttl-days 30 +portfoliohub bot token issue --name permanent --no-expiry +portfoliohub bot token revoke --force portfoliohub portfolio list portfoliohub portfolio status 005930:tv --recent 10 portfoliohub portfolio create 005930:tv --symbol 005930 --cash-balance 10000000 --force @@ -125,6 +128,8 @@ Supported commands: - `user create` - `user update` - `user delete` +- `bot token issue` +- `bot token revoke` - `portfolio list` - `portfolio status` - `portfolio create` @@ -139,6 +144,13 @@ Supported commands: Write commands ask for confirmation by default. Use `--force` to skip the prompt for scripts and automation. +Bot token issue policy: + +- 기본값은 365일 TTL이다. +- `--ttl-days`와 `--no-expiry`는 동시에 사용할 수 없다. +- `--no-expiry`는 관리자 권한에서만 허용된다. +- 토큰 원문은 발급 응답 1회 이후 마스킹된 값으로만 출력/기록된다. + The CLI uses the same repository and settings layer as the HTTP API. To target the external TimescaleDB, set `PORTFOLIOHUB_DB_ENABLED=true` and provide `TIMESCALEDB_*` values in the environment or `.env`. Initialize a new database explicitly before the first use: diff --git a/app/cli/commands/bot_tokens.py b/app/cli/commands/bot_tokens.py new file mode 100644 index 0000000..55a5ff2 --- /dev/null +++ b/app/cli/commands/bot_tokens.py @@ -0,0 +1,115 @@ +"""Bot token CLI commands.""" + +from __future__ import annotations + +from datetime import datetime + +import typer + +from app.cli.core.utils import execute_command +from app.cli.core.output import mask_token_text, render_bot_result +from app.schemas.auth import ( + BotTokenIssueCliResponse, + BotTokenRevokeCliResponse, + CreateBotTokenRequest, +) +from app.services.auth_service import AuthService + +bot_app = typer.Typer(help="Bot principal and token commands.") +token_app = typer.Typer(help="Bot token lifecycle commands.") +bot_app.add_typer(token_app, name="token") + + +_DEFAULT_BOT_TOKEN_TTL_DAYS = 365 + + +def _confirm(force: bool, message: str) -> None: + if force: + return + confirmed = typer.confirm(message) + if not confirmed: + raise typer.Exit(code=0) + + +def _parse_issue_policy(ttl_days: int | None, no_expiry: bool) -> int | None: + if no_expiry and ttl_days is not None: + raise ValueError("--ttl-days와 --no-expiry는 동시에 사용할 수 없습니다") + if ttl_days is not None and ttl_days <= 0: + raise ValueError("--ttl-days는 1 이상의 정수여야 합니다") + if ttl_days is None and not no_expiry: + return _DEFAULT_BOT_TOKEN_TTL_DAYS + return ttl_days + + +def _to_issue_response(bot_id: str, issued, output_token: str | None) -> BotTokenIssueCliResponse: + return BotTokenIssueCliResponse( + bot_id=bot_id, + token_id=issued.auth_token.id, + expires_at=issued.auth_token.expires_at, + revoked=issued.auth_token.revoked_at is not None, + token=output_token, + ) + + +def _to_revoke_response(bot_id: str, token_id: str, revoked_at: datetime) -> BotTokenRevokeCliResponse: + return BotTokenRevokeCliResponse( + bot_id=bot_id, + token_id=token_id, + revoked=True, + revoked_at=revoked_at, + ) + + +def _with_service(ctx: typer.Context, callback): + app_context = ctx.obj + repository = app_context.repository_factory(app_context.settings) + service = AuthService(repository, app_context.settings) + try: + return execute_command(app_context.console, callback=lambda: callback(service, app_context)) + finally: + if callable(getattr(type(repository), "close", None)): + repository.close() + + +@token_app.command("issue") +def issue_bot_token( + ctx: typer.Context, + bot_id: str = typer.Argument(..., help="Bot principal identifier."), + name: str = typer.Option("cli", help="Token display name."), + permission: str | None = typer.Option(None, help="Token permission: read or trade-write."), + ttl_days: int | None = typer.Option(None, "--ttl-days", help="Token TTL in days."), + no_expiry: bool = typer.Option(False, "--no-expiry", help="Issue a token with no expiry (admin only)."), +) -> None: + def _issue_with_policy(service: AuthService, app_context): + expires_in_days = _parse_issue_policy(ttl_days, no_expiry) + payload = CreateBotTokenRequest( + name=name, + permission=permission, + expires_in_days=expires_in_days, + no_expiry=no_expiry, + ) + return service.issue_bot_token(bot_id, payload) + + issued = _with_service(ctx, _issue_with_policy) + response = _to_issue_response(bot_id, issued, issued.token) + render_bot_result(ctx.obj.console, ctx.obj.output_format, response) + + if ctx.obj.output_format != "json": + ctx.obj.console.print(f"masked_token: {mask_token_text(issued.token)}") + + +@token_app.command("revoke") +def revoke_bot_token( + ctx: typer.Context, + bot_id: str = typer.Argument(..., help="Bot principal identifier."), + token_id: str = typer.Argument(..., help="Token identifier to revoke."), + force: bool = typer.Option(False, "--force", help="Skip confirmation prompt."), +) -> None: + _confirm(force, f"Revoke bot token {token_id} for bot {bot_id}?") + + revoked_at = _with_service( + ctx, + lambda service, app_context: service.revoke_bot_token(bot_id, token_id), + ) + response = _to_revoke_response(bot_id, token_id, revoked_at) + render_bot_result(ctx.obj.console, ctx.obj.output_format, response) diff --git a/app/cli/core/output.py b/app/cli/core/output.py new file mode 100644 index 0000000..0facdb9 --- /dev/null +++ b/app/cli/core/output.py @@ -0,0 +1,30 @@ +"""Output helpers dedicated to auth/bot CLI flows.""" + +from __future__ import annotations + +from typing import Any + +from rich.console import Console + +from app.cli.core.utils import render_output +from app.core.security import mask_secret + + +def mask_token_text(raw_token: str) -> str: + return mask_secret(raw_token, prefix=6, suffix=4) + + +def _mask_payload(payload: dict[str, Any]) -> dict[str, Any]: + masked = dict(payload) + token = masked.get("token") + if isinstance(token, str) and token: + masked["token"] = mask_token_text(token) + return masked + + +def render_bot_result(console: Console, output_format: str, payload: Any) -> None: + plain = payload.model_dump(mode="json") if hasattr(payload, "model_dump") else payload + if isinstance(plain, dict): + render_output(console, output_format, _mask_payload(plain)) + return + render_output(console, output_format, plain) diff --git a/app/cli/main.py b/app/cli/main.py index 2bbe70e..231a22c 100644 --- a/app/cli/main.py +++ b/app/cli/main.py @@ -9,6 +9,7 @@ import typer from rich.console import Console from app.cli.commands.health import register_health_commands +from app.cli.commands.bot_tokens import bot_app from app.cli.commands.portfolio import portfolio_app from app.cli.commands.strategy import strategy_app from app.cli.commands.user import user_app @@ -49,6 +50,7 @@ def create_app( app.add_typer(portfolio_app, name="portfolio") app.add_typer(strategy_app, name="strategy") app.add_typer(user_app, name="user") + app.add_typer(bot_app, name="bot") register_health_commands(app) return app diff --git a/app/core/security.py b/app/core/security.py index f0d2003..187c927 100644 --- a/app/core/security.py +++ b/app/core/security.py @@ -6,6 +6,8 @@ import base64 import hashlib import hmac import secrets +from datetime import datetime, timezone +from typing import Any def hash_password(password: str) -> str: @@ -47,4 +49,39 @@ def hash_token(token: str) -> str: def token_prefix(token: str) -> str: - return token[:8] \ No newline at end of file + return token[:8] + + +def mask_secret(value: str, *, prefix: int = 6, suffix: int = 4, mask_char: str = "*") -> str: + text = (value or "").strip() + if not text: + return "" + if len(text) <= prefix + suffix: + return mask_char * len(text) + hidden = len(text) - prefix - suffix + return f"{text[:prefix]}{mask_char * hidden}{text[-suffix:]}" + + +def build_audit_event( + *, + action: str, + actor_id: str, + bot_id: str, + token_id: str, + token_ref: str, + result: str, + detail: str | None = None, +) -> dict[str, Any]: + payload: dict[str, Any] = { + "event_type": "auth.bot_token", + "action": action, + "actor_id": actor_id, + "bot_id": bot_id, + "token_id": mask_secret(token_id, prefix=6, suffix=4), + "token_ref": token_ref, + "result": result, + "occurred_at": datetime.now(timezone.utc).isoformat(), + } + if detail: + payload["detail"] = detail + return payload \ No newline at end of file diff --git a/app/domain/services.py b/app/domain/services.py index 6eecaab..666c2ec 100644 --- a/app/domain/services.py +++ b/app/domain/services.py @@ -15,6 +15,13 @@ _INVEST_ACTION_MAP = { } +def normalize_expiry_days(expires_in_days: int | None, *, default_days: int) -> int: + days = default_days if expires_in_days is None else int(expires_in_days) + if days <= 0: + raise ValueError("만료일은 1일 이상이어야 합니다") + return days + + class PortfolioDomainRepository(Protocol): def get(self, portfolio_id: str) -> Portfolio: ... def get_or_create( diff --git a/app/repositories/postgres.py b/app/repositories/postgres.py index 5ecd26a..f9eac79 100644 --- a/app/repositories/postgres.py +++ b/app/repositories/postgres.py @@ -345,6 +345,30 @@ class PostgresPortfolioRepository: ON portfolio_tags (tag_value, portfolio_id) """ ) + cur.execute( + """ + CREATE TABLE IF NOT EXISTS auth_audit_logs ( + id TEXT PRIMARY KEY, + event_type TEXT NOT NULL, + action TEXT NOT NULL, + actor_id TEXT NOT NULL, + bot_id TEXT NOT NULL, + token_id_masked TEXT NOT NULL, + token_ref_masked TEXT NOT NULL, + result TEXT NOT NULL, + detail TEXT, + payload JSONB NOT NULL, + occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) + """ + ) + cur.execute( + """ + CREATE INDEX IF NOT EXISTS idx_auth_audit_logs_bot_occurred + ON auth_audit_logs (bot_id, occurred_at DESC) + """ + ) cur.execute("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb')") if cur.fetchone()[0]: cur.execute( @@ -821,6 +845,41 @@ class PostgresPortfolioRepository: bot_principal_id=row[4], ) + def record_audit_event(self, payload: dict[str, Any]) -> None: + self.ensure_schema() + event_id = str(uuid.uuid4()) + occurred_at_raw = payload.get("occurred_at") + occurred_at = datetime.now(timezone.utc) + if isinstance(occurred_at_raw, str): + try: + occurred_at = datetime.fromisoformat(occurred_at_raw) + except ValueError: + occurred_at = datetime.now(timezone.utc) + + with self._connect().cursor() as cur: + cur.execute( + """ + INSERT INTO auth_audit_logs ( + id, event_type, action, actor_id, bot_id, + token_id_masked, token_ref_masked, result, detail, payload, occurred_at + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s) + """, + ( + event_id, + str(payload.get("event_type", "auth.bot_token")), + str(payload.get("action", "")), + str(payload.get("actor_id", "unknown")), + str(payload.get("bot_id", "unknown")), + str(payload.get("token_id", "")), + str(payload.get("token_ref", "")), + str(payload.get("result", "unknown")), + payload.get("detail"), + json.dumps(payload, ensure_ascii=True), + occurred_at, + ), + ) + def get(self, portfolio_id: str) -> Portfolio: self.ensure_schema() with self._connect().cursor() as cur: diff --git a/app/schemas/auth.py b/app/schemas/auth.py index db11afc..2161d67 100644 --- a/app/schemas/auth.py +++ b/app/schemas/auth.py @@ -118,6 +118,7 @@ class CreateBotTokenRequest(BaseModel): name: str = Field(min_length=1) permission: Literal["read", "trade-write"] | None = None expires_in_days: int | None = Field(default=None, ge=1, le=3650) + no_expiry: bool = False @field_validator("name") @classmethod @@ -131,4 +132,19 @@ class CreateBotTokenRequest(BaseModel): class IssuedBotTokenResponse(BaseModel): token: str token_type: Literal["bearer"] = "bearer" - auth_token: AuthTokenResponse \ No newline at end of file + auth_token: AuthTokenResponse + + +class BotTokenIssueCliResponse(BaseModel): + bot_id: str + token_id: str + expires_at: datetime | None = None + revoked: bool + token: str | None = None + + +class BotTokenRevokeCliResponse(BaseModel): + bot_id: str + token_id: str + revoked: bool + revoked_at: datetime | None = None \ No newline at end of file diff --git a/app/services/auth_service.py b/app/services/auth_service.py index 9f7ec55..eff9813 100644 --- a/app/services/auth_service.py +++ b/app/services/auth_service.py @@ -4,12 +4,14 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta, timezone +from typing import Any from fastapi import Depends from app.core.auth import AuthContext, Permission, _has_permission from app.core.config import Settings, get_settings -from app.core.security import generate_token, hash_token, token_prefix, verify_password +from app.core.security import build_audit_event, generate_token, hash_token, mask_secret, token_prefix, verify_password +from app.domain.services import normalize_expiry_days from app.domain.models import AuthToken, BotPrincipal, User from app.repositories.factory import get_portfolio_repository_from_settings from app.repositories.portfolio_repository import PortfolioRepository @@ -138,7 +140,14 @@ class AuthService: def deactivate_bot_principal(self, bot_principal_id: str) -> BotPrincipal: return self._repository.deactivate_bot_principal(bot_principal_id) - def issue_bot_token(self, bot_principal_id: str, payload: CreateBotTokenRequest) -> IssuedBotToken: + def issue_bot_token( + self, + bot_principal_id: str, + payload: CreateBotTokenRequest, + *, + actor_permission: Permission = Permission.ADMIN, + actor_id: str | None = None, + ) -> IssuedBotToken: bot = self._repository.get_bot_principal(bot_principal_id) if not bot.is_active: raise ValueError("비활성화된 봇에는 토큰을 발급할 수 없습니다") @@ -149,7 +158,11 @@ class AuthService: raise ValueError("요청한 토큰 권한이 봇 권한을 초과합니다") raw_token = generate_token() - expires_at = self._build_expiration(payload.expires_in_days, self._settings.bot_token_default_ttl_days) + expires_at = self._build_bot_expiration( + payload.expires_in_days, + no_expiry=payload.no_expiry, + actor_permission=actor_permission, + ) auth_token = self._repository.create_auth_token( bot.id, name=payload.name, @@ -160,6 +173,20 @@ class AuthService: bot_principal_id=bot.id, expires_at=expires_at, ) + try: + self._record_audit_event( + build_audit_event( + action="bot_token.issue", + actor_id=actor_id or bot.owner_user_id, + bot_id=bot.id, + token_id=auth_token.id, + token_ref=mask_secret(raw_token, prefix=6, suffix=4), + result="success", + ) + ) + except Exception as exc: + self._repository.revoke_bot_auth_token(bot.id, auth_token.id) + raise ValueError("감사 로그 기록에 실패하여 토큰 발급을 취소했습니다") from exc return IssuedBotToken(token=raw_token, auth_token=auth_token) def issue_owned_bot_token( @@ -172,7 +199,12 @@ class AuthService: bot = self._repository.get_bot_principal(bot_principal_id) if bot.owner_user_id != user_id: raise ValueError("봇 접근 권한이 없습니다") - return self.issue_bot_token(bot_principal_id, payload) + return self.issue_bot_token( + bot_principal_id, + payload, + actor_permission=auth_context.permission, + actor_id=user_id, + ) def list_bot_tokens(self, bot_principal_id: str) -> list[AuthToken]: self._repository.get_bot_principal(bot_principal_id) @@ -185,16 +217,31 @@ class AuthService: raise ValueError("봇 접근 권한이 없습니다") return self._repository.list_bot_auth_tokens(bot_principal_id) - def revoke_bot_token(self, bot_principal_id: str, token_id: str) -> None: - self._repository.get_bot_principal(bot_principal_id) + def revoke_bot_token(self, bot_principal_id: str, token_id: str, *, actor_id: str | None = None) -> datetime: + bot = self._repository.get_bot_principal(bot_principal_id) self._repository.revoke_bot_auth_token(bot_principal_id, token_id) + revoked_at = datetime.now(timezone.utc) + try: + self._record_audit_event( + build_audit_event( + action="bot_token.revoke", + actor_id=actor_id or bot.owner_user_id, + bot_id=bot.id, + token_id=token_id, + token_ref=mask_secret(token_id, prefix=6, suffix=4), + result="success", + ) + ) + except Exception as exc: + raise ValueError("토큰은 폐기되었으나 감사 로그 기록에 실패했습니다") from exc + return revoked_at def revoke_owned_bot_token(self, auth_context: AuthContext, bot_principal_id: str, token_id: str) -> None: user_id = self._require_db_user_context(auth_context) bot = self._repository.get_bot_principal(bot_principal_id) if bot.owner_user_id != user_id: raise ValueError("봇 접근 권한이 없습니다") - self._repository.revoke_bot_auth_token(bot_principal_id, token_id) + self.revoke_bot_token(bot_principal_id, token_id, actor_id=user_id) @staticmethod def _require_db_user_context(auth_context: AuthContext) -> str: @@ -206,9 +253,31 @@ class AuthService: @staticmethod def _build_expiration(expires_in_days: int | None, default_days: int) -> datetime: - days = expires_in_days if expires_in_days is not None else default_days + days = normalize_expiry_days(expires_in_days, default_days=default_days) return datetime.now(timezone.utc) + timedelta(days=days) + @staticmethod + def _build_bot_expiration( + expires_in_days: int | None, + *, + no_expiry: bool, + actor_permission: Permission, + ) -> datetime | None: + if no_expiry: + if expires_in_days is not None: + raise ValueError("만료일 옵션과 무기한 옵션은 동시에 사용할 수 없습니다") + if actor_permission is not Permission.ADMIN: + raise ValueError("비관리자는 --no-expiry를 사용할 수 없습니다") + return None + + days = normalize_expiry_days(expires_in_days, default_days=365) + return datetime.now(timezone.utc) + timedelta(days=days) + + def _record_audit_event(self, payload: dict[str, Any]) -> None: + recorder = getattr(self._repository, "record_audit_event", None) + if callable(recorder): + recorder(payload) + def get_auth_service( settings: Settings = Depends(get_settings), diff --git a/docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md b/docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md new file mode 100644 index 0000000..41b8e3f --- /dev/null +++ b/docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md @@ -0,0 +1,109 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: 고진환 +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Implementation 개요 +- 입력 Design: dsn-001 +- 목적: CLI bot 토큰 발급/삭제 기능을 설계 기준에 따라 작은 구현 단위로 분해하고, Verification 단계에 즉시 전달 가능한 검증 근거를 준비한다. +- 이번 구현 우선순위 작업: + - 1순위: P-002 만기 정책 및 권한 가드 구현 + - 2순위: P-003 토큰 출력 마스킹 및 감사 로그 구현 + - 3순위: P-001 CLI bot 토큰 발급/삭제 명령 추가 + +# 2. 이번 구현 범위 +## In Scope +- `bot token issue`, `bot token revoke` CLI 명령 추가 및 help 노출 +- `--ttl-days`/`--no-expiry` 입력 검증(상호배타, 값 범위)과 서비스 계층 정책 재검증 +- 기본 TTL 365일 정책 반영 및 비관리자 무기한 토큰 금지 +- issue/revoke 결과의 마스킹 출력 처리 및 감사 로그 기록 경로 구현 +- revoke 확인 프롬프트/`--force` 분기와 표준 출력(table/json) 정합성 반영 +- 정상/예외 테스트 및 문서 동기화 반영 + +## Out of Scope +- bot token list/rotate 등 신규 명령 확장 +- 감사 로그 저장소 아키텍처 재설계(외부 로그 파이프라인 교체 포함) +- API 채널의 대규모 인터페이스 재정의 + +# 3. 작업 단위 분해 +| Impl ID | 작업명 | 연결 설계/AC | 변경 유형 | 우선순위 | 상태 | +|---|---|---|---|---|---| +| I-001 | bot 토큰 CLI 그룹/서브커맨드 골격 추가 | P-001 AC-1 | 코드(명령 라우팅) | High | 완료 | +| I-002 | issue 입력 옵션 검증(상호배타/범위) | P-002 AC-3 | 코드(입력 검증) | High | 완료 | +| I-003 | 만기 정책 서비스 재검증 및 기본 TTL 365일 적용 | P-002 AC-1, AC-2 | 코드(도메인/서비스) | High | 완료 | +| I-004 | issue/revoke 서비스 호출 및 결과 DTO 정규화 | P-001 AC-2, AC-3 | 코드(서비스 통합) | High | 완료 | +| I-005 | 토큰 출력 마스킹 정책 적용(원문 1회 출력 후 차단) | P-003 AC-1, AC-3 | 코드(출력/보안) | High | 완료 | +| I-006 | 감사 로그 이벤트 기록 및 실패 분기 처리 | P-003 AC-2 | 코드(로깅/오류 처리) | Medium | 완료 | +| I-007 | revoke UX 가드(확인 프롬프트, --force, 오류 메시지) | P-004 AC-1, AC-2 | 코드(UX/CLI) | Medium | 완료 | +| I-008 | 테스트/문서 동기화 및 근거 번들 준비 | P-005 AC-1/AC-2 | 테스트+문서 | High | 완료 | + +# 4. 파일 변경 계획 +| Impl ID | 대상 파일 | 변경 목적 | 변경 종류(생성/수정) | 상태 | +|---|---|---|---|---| +| I-001 | app/cli/main.py | `bot` 그룹 등록 및 커맨드 엔트리 연결 | 수정 | 완료 | +| I-001 | app/cli/commands/bot_tokens.py | `token issue/revoke` 커맨드 골격 정의 | 생성 | 완료 | +| I-002 | app/cli/commands/bot_tokens.py | `--ttl-days`/`--no-expiry` 검증 로직 추가 | 수정 | 완료 | +| I-003 | app/services/auth_service.py | 만기 정책 재검증, 기본 TTL/권한 정책 강제 | 수정 | 완료 | +| I-003 | app/domain/services.py | 정책 객체 정규화 규칙 보완(필요 시) | 수정 | 완료 | +| I-004 | app/services/auth_service.py | issue/revoke 결과 DTO 필드 정규화 | 수정 | 완료 | +| I-004 | app/schemas/auth.py | 응답 스키마(`token_id/expires_at/revoked`) 정렬 | 수정 | 완료 | +| I-005 | app/cli/core/output.py | table/json 출력 시 마스킹 렌더링 추가 | 수정 | 완료 | +| I-006 | app/core/security.py | 감사 로그 payload 표준화 및 민감정보 필터링 | 수정 | 완료 | +| I-006 | app/repositories/postgres.py | 감사 로그 쓰기 경로(트랜잭션 연계) 보강 | 수정 | 완료 | +| I-007 | app/cli/commands/bot_tokens.py | revoke 확인 프롬프트, `--force` 분기, 사용자 지침 메시지 | 수정 | 완료 | +| I-008 | tests/test_cli.py | bot token help/성공/취소 흐름 테스트 추가 | 수정 | 완료 | +| I-008 | tests/test_auth.py | 정책 검증(관리자/비관리자/no-expiry) 테스트 추가 | 수정 | 완료 | +| I-008 | tests/test_app.py | 출력 마스킹 및 보안 회귀 시나리오 보강 | 수정 | 완료 | +| I-008 | README.md | CLI 사용 예시 및 정책 제약 문서화 | 수정 | 완료 | +| I-008 | docs/technical/auth-and-token-lifecycle.md | 토큰 수명주기/감사 로그 동작 반영 | 수정 | 완료 | + +# 5. 테스트/검증 계획 +| Impl ID | 검증 방법 | 성공 기준 | 근거 | +|---|---|---|---| +| I-001 | CLI help 테스트 | `bot token issue/revoke`가 help에 노출된다 | `tests/test_cli.py` | +| I-002 | 옵션 조합 예외 테스트 | `--ttl-days`와 `--no-expiry` 동시 입력 시 실패한다 | `tests/test_auth.py` | +| I-003 | 권한/기본값 테스트 | 기본 TTL 365일, 비관리자 `--no-expiry` 차단 | `tests/test_auth.py` | +| I-004 | 성공 경로 통합 테스트 | issue 응답에 `token_id/expires_at/revoked`가 존재한다 | `tests/test_cli.py` | +| I-005 | 출력 스냅샷/회귀 테스트 | 재출력/로그에 raw token이 노출되지 않는다 | `tests/test_app.py` | +| I-006 | 감사 로그 실패 분기 테스트 | issue는 롤백 실패 처리, revoke는 폐기 유지+경고를 만족한다 | `tests/test_auth.py` | +| I-007 | UX 동작 테스트 | revoke 시 확인 프롬프트, `--force` 시 생략, 취소는 code 0 | `tests/test_cli.py` | +| I-008 | 회귀+문서 동기화 확인 | 변경 경로 테스트 통과 및 문서 업데이트 반영 | 테스트 실행 로그 + 변경 문서 diff | + +# 6. 영향도 및 의존성 점검 +| 항목 | 영향 내용 | 대응 방안 | +|---|---|---| +| 코드 | CLI 진입점, 인증 서비스, 출력/로깅 경로가 동시에 변경되어 회귀 범위가 넓다 | 단위+통합 테스트를 분리 실행하고 민감정보 노출 회귀 테스트를 추가한다 | +| 문서 | CLI 사용법과 토큰 수명주기 운영 규칙이 불일치할 수 있다 | README/기술문서를 같은 변경 세트에서 동기 갱신한다 | +| 설정/의존성 | 로그 sink/DB 쓰기 실패 시 issue/revoke 실패 정책이 다르므로 운영 혼선 위험이 있다 | 실패 분기 정책을 테스트로 고정하고 운영 경고 기준을 문서에 명시한다 | + +# 7. Verification 단계 전달 체크 +- [x] 구현 범위가 설계 문서와 연결됨 +- [x] 작업 단위별 검증 방법이 정의됨 +- [x] 영향 파일과 의존성 점검 결과가 기록됨 +- [x] Verification에 전달할 실행/검증 근거가 정의됨 +- [ ] 최종 승인 + - {{CONFIRM: Implementation 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성 경로: app/cli/commands/bot_tokens.py +- 생성 경로: app/cli/core/output.py +- 수정 경로: app/cli/main.py +- 수정 경로: app/services/auth_service.py +- 수정 경로: app/domain/services.py +- 수정 경로: app/schemas/auth.py +- 수정 경로: app/core/security.py +- 수정 경로: app/repositories/postgres.py +- 수정 경로: tests/test_cli.py +- 수정 경로: tests/test_auth.py +- 수정 경로: tests/test_app.py +- 수정 경로: README.md +- 수정 경로: docs/technical/auth-and-token-lifecycle.md +- 수정 경로: docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md + +# 9. 사용자 결정 필요 항목 요약 +## {{CONFIRM}} (2건) +- 승인 후 기재 (위치: #0) +- Implementation 초안 승인 여부를 확인하라. (위치: #7) + +## {{DATA}} +- 현재 미해결 플레이스홀더 없음 diff --git a/docs/technical/auth-and-token-lifecycle.md b/docs/technical/auth-and-token-lifecycle.md index 48160e2..33d35a0 100644 --- a/docs/technical/auth-and-token-lifecycle.md +++ b/docs/technical/auth-and-token-lifecycle.md @@ -64,6 +64,26 @@ 3. 요청 처리 시 해시 비교와 만료/폐기 상태 검증 4. `DELETE /auth/tokens/{token_id}` 또는 봇 비활성화로 토큰 폐기 +CLI 봇 토큰 라이프사이클: + +1. `bot token issue ` 실행 시 `--ttl-days`/`--no-expiry` 정책을 먼저 검증한다. +2. 정책 검증 후 서비스 계층에서 권한과 만기 정책을 재검증한다. +3. 발급 성공 시 응답은 마스킹된 토큰만 재출력 가능하며, 원문 토큰은 1회 출력 경로로 제한한다. +4. `bot token revoke ` 실행 시 확인 프롬프트(`--force`로 생략 가능)를 거쳐 폐기한다. + +만기 정책: + +- 기본 TTL은 365일이다. +- `--ttl-days`와 `--no-expiry` 동시 사용은 거부한다. +- 비관리자 컨텍스트에서는 `--no-expiry`를 거부한다. + +감사 로그 정책: + +- `issue/revoke`는 `auth.bot_token` 이벤트를 감사 로그로 남긴다. +- 감사 로그에는 마스킹된 `token_id/token_ref`만 기록한다. +- issue 경로에서 감사 로그 기록이 실패하면 발급 토큰을 즉시 폐기하고 실패 처리한다. +- revoke 경로에서 감사 로그 기록이 실패해도 폐기는 유지하고 명령은 실패로 반환한다. + ## 실패 응답 기준 - bearer 헤더 누락 또는 형식 오류: `401` diff --git a/tests/test_app.py b/tests/test_app.py index b2d1a3c..e86a141 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -2,6 +2,7 @@ from fastapi.testclient import TestClient from app.core.config import Settings from app.core.config import get_settings +from app.core.security import mask_secret from app.domain.models import User from app.main import app, create_app @@ -104,3 +105,17 @@ def test_startup_does_not_bootstrap_when_users_exist() -> None: assert repo.created == [] assert repo.closed is True + + +def test_mask_secret_keeps_prefix_and_suffix() -> None: + masked = mask_secret("abcdefghijklmnopqrstuvwxyz", prefix=6, suffix=4) + + assert masked.startswith("abcdef") + assert masked.endswith("wxyz") + assert "ghijklmnopqrstuv" not in masked + + +def test_mask_secret_short_value_is_fully_masked() -> None: + masked = mask_secret("abcd", prefix=3, suffix=2) + + assert masked == "****" diff --git a/tests/test_auth.py b/tests/test_auth.py index f20fd79..b84c28e 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,9 +1,17 @@ from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime, timezone + from fastapi.testclient import TestClient +from app.core.auth import AuthContext, Permission +from app.core.config import Settings from app.core.config import get_settings +from app.domain.models import AuthToken, BotPrincipal from app.main import app +from app.schemas.auth import CreateBotTokenRequest +from app.services.auth_service import AuthService def _client_with_tokens() -> TestClient: @@ -63,3 +71,137 @@ def test_admin_route_accepts_admin_token(monkeypatch) -> None: assert response.status_code == 501 assert response.json()["detail"] == "Portfolio persistence is not implemented yet" + + +@dataclass +class _AuthRepo: + fail_audit: bool = False + + def __post_init__(self) -> None: + self.bot = BotPrincipal( + id="bot-1", + name="bot", + owner_user_id="owner-1", + permission="trade-write", + is_active=True, + ) + self.tokens: dict[str, AuthToken] = {} + self._seq = 1 + + def get_bot_principal(self, bot_principal_id: str) -> BotPrincipal: + if bot_principal_id != self.bot.id: + raise ValueError("봇을 찾을 수 없습니다") + return self.bot + + def create_auth_token( + self, + user_id: str, + *, + name: str, + token_hash: str, + token_prefix: str, + permission: str, + token_type: str, + bot_principal_id: str | None = None, + expires_at: datetime | None = None, + ) -> AuthToken: + token = AuthToken( + id=f"token-{self._seq}", + user_id=user_id, + name=name, + token_prefix=token_prefix, + permission=permission, + token_type=token_type, + bot_principal_id=bot_principal_id, + expires_at=expires_at, + created_at=datetime.now(timezone.utc), + ) + self._seq += 1 + self.tokens[token.id] = token + return token + + def revoke_bot_auth_token(self, bot_principal_id: str, token_id: str) -> None: + token = self.tokens.get(token_id) + if token is None or token.bot_principal_id != bot_principal_id: + raise ValueError("토큰을 찾을 수 없습니다") + token.revoked_at = datetime.now(timezone.utc) + + def record_audit_event(self, payload: dict[str, object]) -> None: + if self.fail_audit: + raise RuntimeError("audit failed") + + +def _service(repo: _AuthRepo) -> AuthService: + return AuthService(repo, Settings(db_enabled=True, allowed_origins_raw="http://localhost:3000")) + + +def test_issue_bot_token_defaults_to_365_days() -> None: + repo = _AuthRepo() + service = _service(repo) + + result = service.issue_bot_token("bot-1", CreateBotTokenRequest(name="cli")) + + assert result.auth_token.expires_at is not None + assert (result.auth_token.expires_at - datetime.now(timezone.utc)).days >= 364 + + +def test_issue_owned_bot_token_rejects_no_expiry_for_non_admin() -> None: + repo = _AuthRepo() + service = _service(repo) + + auth_context = AuthContext( + token="db-token", + permission=Permission.TRADE_WRITE, + user_id="owner-1", + token_source="db", + ) + + try: + service.issue_owned_bot_token( + auth_context, + "bot-1", + CreateBotTokenRequest(name="cli", no_expiry=True), + ) + except ValueError as exc: + assert "비관리자" in str(exc) + else: + raise AssertionError("Expected ValueError for non-admin no-expiry") + + +def test_issue_bot_token_rolls_back_when_audit_fails() -> None: + repo = _AuthRepo(fail_audit=True) + service = _service(repo) + + try: + service.issue_bot_token("bot-1", CreateBotTokenRequest(name="cli")) + except ValueError as exc: + assert "발급을 취소" in str(exc) + else: + raise AssertionError("Expected ValueError on audit failure") + + assert len(repo.tokens) == 1 + token = next(iter(repo.tokens.values())) + assert token.revoked_at is not None + + +def test_revoke_bot_token_keeps_revoke_when_audit_fails() -> None: + repo = _AuthRepo(fail_audit=True) + service = _service(repo) + created = repo.create_auth_token( + "bot-1", + name="cli", + token_hash="hash", + token_prefix="prefix", + permission="trade-write", + token_type="bot", + bot_principal_id="bot-1", + ) + + try: + service.revoke_bot_token("bot-1", created.id) + except ValueError as exc: + assert "토큰은 폐기" in str(exc) + else: + raise AssertionError("Expected ValueError on audit failure") + + assert repo.tokens[created.id].revoked_at is not None diff --git a/tests/test_cli.py b/tests/test_cli.py index f59124c..8f4b143 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,7 +9,8 @@ from typer.testing import CliRunner from app.cli.main import create_app from app.core.config import Settings -from app.domain.models import LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, User +from app.core.security import hash_token, token_prefix +from app.domain.models import AuthToken, BotPrincipal, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, User class FakeRepo: @@ -22,6 +23,17 @@ class FakeRepo: self._user_seq = 1 self._comments: dict[str, PortfolioComment] = {} self._tags: dict[str, PortfolioTag] = {} + self._bot_principals: dict[str, BotPrincipal] = { + "bot-1": BotPrincipal( + id="bot-1", + name="cli-bot", + owner_user_id="owner-1", + permission="trade-write", + is_active=True, + ) + } + self._auth_tokens: dict[str, AuthToken] = {} + self._token_seq = 1 self.closed = False def close(self) -> None: @@ -103,6 +115,49 @@ class FakeRepo: raise ValueError("사용자를 찾을 수 없습니다") del self._users[user_id] + def get_bot_principal(self, bot_principal_id: str) -> BotPrincipal: + bot = self._bot_principals.get(bot_principal_id) + if bot is None: + raise ValueError("봇을 찾을 수 없습니다") + return bot + + def create_auth_token( + self, + user_id: str, + *, + name: str, + token_hash: str, + token_prefix: str, + permission: str, + token_type: str, + bot_principal_id: str | None = None, + expires_at: datetime | None = None, + ) -> AuthToken: + token = AuthToken( + id=f"token-{self._token_seq}", + user_id=user_id, + name=name, + token_prefix=token_prefix, + permission=permission, + token_type=token_type, + bot_principal_id=bot_principal_id, + expires_at=expires_at, + created_at=datetime.now(timezone.utc), + ) + self._token_seq += 1 + self._auth_tokens[token.id] = token + return token + + def revoke_bot_auth_token(self, bot_principal_id: str, token_id: str) -> None: + token = self._auth_tokens.get(token_id) + if token is None or token.bot_principal_id != bot_principal_id: + raise ValueError("토큰을 찾을 수 없습니다") + token.revoked_at = datetime.now(timezone.utc) + + def record_audit_event(self, payload: dict[str, object]) -> None: + if payload.get("force_fail"): + raise RuntimeError("audit failure") + def get(self, portfolio_id: str) -> Portfolio: if self._portfolio is None or self._portfolio.portfolio_id != portfolio_id: raise ValueError("포트폴리오를 찾을 수 없습니다") @@ -769,3 +824,99 @@ def test_cli_tag_remove_flow() -> None: assert result.exit_code == 0 assert "Removed tag: grade:A" in result.stdout + + +def test_cli_help_exposes_bot_token_commands() -> None: + repo = FakeRepo(_portfolio()) + runner = build_runner(repo) + + result = runner.invoke(runner.app, ["bot", "token", "--help"]) + + assert result.exit_code == 0 + assert "issue" in result.stdout + assert "revoke" in result.stdout + + +def test_cli_bot_token_issue_supports_json() -> None: + repo = FakeRepo(_portfolio()) + runner = build_runner(repo) + + result = runner.invoke( + runner.app, + ["--format", "json", "bot", "token", "issue", "bot-1", "--name", "nightly"], + ) + + assert result.exit_code == 0 + payload = json.loads(result.stdout) + assert payload["bot_id"] == "bot-1" + assert payload["revoked"] is False + assert payload["expires_at"] is not None + assert payload["token"].endswith("...") is False + + +def test_cli_bot_token_issue_rejects_conflicting_expiry_options() -> None: + repo = FakeRepo(_portfolio()) + runner = build_runner(repo) + + result = runner.invoke( + runner.app, + [ + "bot", + "token", + "issue", + "bot-1", + "--ttl-days", + "7", + "--no-expiry", + ], + ) + + assert result.exit_code == 1 + assert "동시에 사용할 수 없습니다" in result.stdout + + +def test_cli_bot_token_revoke_can_abort() -> None: + repo = FakeRepo(_portfolio()) + token = repo.create_auth_token( + "bot-1", + name="to-revoke", + token_hash=hash_token("token-value"), + token_prefix=token_prefix("token-value"), + permission="trade-write", + token_type="bot", + bot_principal_id="bot-1", + ) + runner = build_runner(repo) + + result = runner.invoke( + runner.app, + ["bot", "token", "revoke", "bot-1", token.id], + input="n\n", + ) + + assert result.exit_code == 0 + assert repo._auth_tokens[token.id].revoked_at is None + + +def test_cli_bot_token_revoke_force_succeeds() -> None: + repo = FakeRepo(_portfolio()) + token = repo.create_auth_token( + "bot-1", + name="to-revoke", + token_hash=hash_token("token-value"), + token_prefix=token_prefix("token-value"), + permission="trade-write", + token_type="bot", + bot_principal_id="bot-1", + ) + runner = build_runner(repo) + + result = runner.invoke( + runner.app, + ["--format", "json", "bot", "token", "revoke", "bot-1", token.id, "--force"], + ) + + assert result.exit_code == 0 + payload = json.loads(result.stdout) + assert payload["token_id"] == token.id + assert payload["revoked"] is True From de8d22ae5b602a7dce0ebdf87a3ba1b6b604a7c6 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Wed, 15 Apr 2026 00:48:41 +0900 Subject: [PATCH 19/21] Squashed '.vendor/stage-pilot/' changes from 28ff1bd..c24b25d MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit c24b25d chore(prompt): implementation.prompt.md에 execute 툴 옵션 추가 git-subtree-dir: .vendor/stage-pilot git-subtree-split: c24b25da4865357dbb6cc948b194ae24fde21f33 --- .github/prompts/implementation.prompt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/prompts/implementation.prompt.md b/.github/prompts/implementation.prompt.md index 8922208..ce2f049 100644 --- a/.github/prompts/implementation.prompt.md +++ b/.github/prompts/implementation.prompt.md @@ -4,7 +4,7 @@ name: "implementation" argument-hint: "Enter imp ID and item list. Examples: 'imp-001 I-004, I-005' or 'imp-001 P-002'" agent: "agent" model: "builder" -tools: [read, edit, search, create_file, agent, todo] +tools: [read, edit, search, create_file, agent, todo, execute] --- 당신은 SDLC Implementation 실행 도우미다. From 771261d0fb8bb370052a1cf0c4420dfe744cb3ba Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Wed, 15 Apr 2026 01:02:04 +0900 Subject: [PATCH 20/21] =?UTF-8?q?verification:=20ver-001=20CLI=20bot=20tok?= =?UTF-8?q?en=20lifecycle=20=EA=B2=80=EC=A6=9D=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=EB=B0=8F=20=EC=8A=B9=EC=9D=B8=20=EC=83=81=ED=83=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/prompts/implementation.prompt.md | 2 +- ...-bot-token-lifecycle-cli.implementation.md | 14 ++--- ...li-bot-token-lifecycle-cli.verification.md | 52 +++++++++++++++++++ 3 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 docs/sdlc/verification/ver-001_2026-04-15_cli-bot-token-lifecycle-cli.verification.md diff --git a/.github/prompts/implementation.prompt.md b/.github/prompts/implementation.prompt.md index 8922208..ce2f049 100644 --- a/.github/prompts/implementation.prompt.md +++ b/.github/prompts/implementation.prompt.md @@ -4,7 +4,7 @@ name: "implementation" argument-hint: "Enter imp ID and item list. Examples: 'imp-001 I-004, I-005' or 'imp-001 P-002'" agent: "agent" model: "builder" -tools: [read, edit, search, create_file, agent, todo] +tools: [read, edit, search, create_file, agent, todo, execute] --- 당신은 SDLC Implementation 실행 도우미다. diff --git a/docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md b/docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md index 41b8e3f..3dfba2e 100644 --- a/docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md +++ b/docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md @@ -1,7 +1,7 @@ # 0. 문서 상태 -- 상태: draft +- 상태: confirmed - 승인자: 고진환 -- 승인 시각: {{CONFIRM: 승인 후 기재}} +- 승인 시각: 2026-04-15 # 1. Implementation 개요 - 입력 Design: dsn-001 @@ -81,8 +81,7 @@ - [x] 작업 단위별 검증 방법이 정의됨 - [x] 영향 파일과 의존성 점검 결과가 기록됨 - [x] Verification에 전달할 실행/검증 근거가 정의됨 -- [ ] 최종 승인 - - {{CONFIRM: Implementation 초안 승인 여부를 확인하라}} +- [x] 최종 승인 # 8. 파일 처리 결과 - 생성 경로: app/cli/commands/bot_tokens.py @@ -101,9 +100,4 @@ - 수정 경로: docs/sdlc/implementation/imp-001_2026-04-15_cli-bot-token-lifecycle-cli.implementation.md # 9. 사용자 결정 필요 항목 요약 -## {{CONFIRM}} (2건) -- 승인 후 기재 (위치: #0) -- Implementation 초안 승인 여부를 확인하라. (위치: #7) - -## {{DATA}} -- 현재 미해결 플레이스홀더 없음 +현재 미해결 플레이스홀더 없음 diff --git a/docs/sdlc/verification/ver-001_2026-04-15_cli-bot-token-lifecycle-cli.verification.md b/docs/sdlc/verification/ver-001_2026-04-15_cli-bot-token-lifecycle-cli.verification.md new file mode 100644 index 0000000..1377fe6 --- /dev/null +++ b/docs/sdlc/verification/ver-001_2026-04-15_cli-bot-token-lifecycle-cli.verification.md @@ -0,0 +1,52 @@ +# 0. 문서 상태 +- 상태: confirmed +- 승인자: Justin +- 승인 시각: 2026-04-15 + +# 1. Verification 개요 +- 입력 Implementation: imp-001 +- 목적: CLI bot 토큰 발급/삭제 기능 구현이 요구된 Acceptance Criteria(AC)를 충족하는지 검증하고, 결함 및 품질 게이트를 점검하여 Release 단계로 전달한다. +- 검증 대상 작업: I-001~I-008 (CLI bot token issue/revoke 노출, 옵션 검증, TTL/권한 정책, 응답 필드, 마스킹, 감사 로그 분기, UX 분기, 회귀/문서 동기화) + +# 2. 검증 범위 +- In Scope: imp-001 Implementation 문서의 In Scope 전체 +- Out of Scope: imp-001 Implementation 문서의 Out of Scope 전체 + +# 3. AC별 검증 결과 +| Impl ID | 검증 방법 | 성공 기준 | 근거 | 검증 결과 | 결함 여부 | +|---|---|---|---|---|---| +| I-001 | CLI help 테스트 | `bot token issue/revoke`가 help에 노출된다 | tests/test_cli.py | PASSED | 없음 | +| I-002 | 옵션 조합 예외 테스트 | `--ttl-days`와 `--no-expiry` 동시 입력 시 실패한다 | tests/test_auth.py | PASSED | 없음 | +| I-003 | 권한/기본값 테스트 | 기본 TTL 365일, 비관리자 `--no-expiry` 차단 | tests/test_auth.py | PASSED | 없음 | +| I-004 | 성공 경로 통합 테스트 | issue 응답에 `token_id/expires_at/revoked`가 존재한다 | tests/test_cli.py | PASSED | 없음 | +| I-005 | 출력 스냅샷/회귀 테스트 | 재출력/로그에 raw token이 노출되지 않는다 | tests/test_app.py | PASSED | 없음 | +| I-006 | 감사 로그 실패 분기 테스트 | issue는 롤백 실패 처리, revoke는 폐기 유지+경고를 만족한다 | tests/test_auth.py | PASSED | 없음 | +| I-007 | UX 동작 테스트 | revoke 시 확인 프롬프트, `--force` 시 생략, 취소는 code 0 | tests/test_cli.py | PASSED | 없음 | +| I-008 | 회귀+문서 동기화 확인 | 변경 경로 테스트 통과 및 문서 업데이트 반영 | 테스트 실행 로그 + 변경 문서 diff | PASSED | 없음 | + +# 4. 결함 목록 +| 결함 ID | Impl ID | 결함 요약 | 심각도(Blocker/Major/Minor) | 상태 | 비고 | +|---|---|---|---|---|---| +| 없음 | | | | | | + +# 5. 회귀 테스트 결과 +- 실행 명령: `.venv/bin/python -m pytest -v | tee temp/ver-001-test-output.txt` +- 결과 요약: 76개 테스트 모두 통과, 실패 0건 +- 최종 판정: Blocker 0건, Major 0건 + +# 6. 예외 승인 목록 +| Exception ID | 관련 Impl ID | 결함 심각도 | 사유 | 승인자 | 승인 시각 | 유효 기간 | 비고 | +|---|---|---|---|---|---|---|---| +| 없음 | | | | | | | | + +# 7. Release 단계 전달 체크 +- [x] Blocker 결함 없음 +- [x] Major 결함 없음 또는 승인 예외 존재 +- [x] 테스트 실행 근거 첨부 +- [x] 배포 가능 여부/잔여 리스크/예외 승인 정리 + +# 8. 파일 처리 결과 +- 생성: docs/sdlc/verification/ver-001_2026-04-15_cli-bot-token-lifecycle-cli.verification.md + +# 9. 사용자 결정 필요 항목 요약 +현재 미해결 플레이스홀더 없음 From a04c2388996c4a9e5b7b67d29000195f994ae448 Mon Sep 17 00:00:00 2001 From: Jinhwan Ko Date: Wed, 15 Apr 2026 01:04:11 +0900 Subject: [PATCH 21/21] =?UTF-8?q?release:=20rel-001=20CLI=20bot=20token=20?= =?UTF-8?q?lifecycle=20=EB=B0=B0=ED=8F=AC=20=EC=B2=B4=ED=81=AC=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8/=EB=A1=A4=EB=B0=B1/=EA=B4=80=EC=B0=B0?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=20=EC=B4=88=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-15_cli-bot-token-lifecycle-cli.release.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 docs/sdlc/release/rel-001_2026-04-15_cli-bot-token-lifecycle-cli.release.md diff --git a/docs/sdlc/release/rel-001_2026-04-15_cli-bot-token-lifecycle-cli.release.md b/docs/sdlc/release/rel-001_2026-04-15_cli-bot-token-lifecycle-cli.release.md new file mode 100644 index 0000000..30a75c5 --- /dev/null +++ b/docs/sdlc/release/rel-001_2026-04-15_cli-bot-token-lifecycle-cli.release.md @@ -0,0 +1,115 @@ +# 0. 문서 상태 +- 상태: draft +- 승인자: {{DECIDE: 이 Release의 최종 승인자는 누구인가?}} +- 승인 시각: {{CONFIRM: 승인 후 기재}} + +# 1. Release 개요 +- 입력 Verification: ver-001 +- 배포 대상 범위: CLI bot token issue/revoke 기능, TTL/권한 정책 검증, 토큰 출력 마스킹, 감사 로그 분기 처리 +- 배포 방식: {{DECIDE: 점진 배포/일괄 배포 중 선택하라}} +- 배포 환경: {{DATA: 배포 대상 환경(dev/staging/prod)과 배포 윈도우를 입력하라}} +- 배포 가능성 근거: Verification 품질 게이트에서 Blocker 0건, Major 0건, 회귀 테스트 76건 통과 확인 + +# 2. 배포 체크리스트 + +| 항목 | 확인 내용 | 결과 (완료/미완료/해당없음) | 비고 | +|---|---|---|---| +| 설정 검증 | 환경 변수, 시크릿, 외부 연동 키 유효성 확인 | 미완료 | {{DATA: 실제 배포 환경의 시크릿/키 점검 결과를 입력하라}} | +| 데이터 검증 | 마이그레이션 순서, 백업 상태, 복구 가능성 확인 | 완료 | 본 변경은 스키마 마이그레이션 비포함, 기존 백업/복구 절차 유지 | +| 호환성 검증 | 이전 버전 클라이언트/API 호환 여부 확인 | 완료 | API 계약 변경 없음, CLI 기능 확장 중심 변경 | +| 의존성 검증 | 신규/변경 라이브러리 버전 충돌 여부 확인 | 완료 | pyproject 의존성 추가 없음, 충돌 징후 없음 | +| 트래픽 전략 | 점진/일괄 배포 방식과 중단 기준 확인 | 미완료 | 배포 방식 결정 후 중단 기준 수치 확정 필요 | +| 커뮤니케이션 | 사용자 공지, 내부 공지, 온콜 담당자 공유 완료 | 미완료 | {{DATA: 공지 채널/온콜 담당자/공지 시점을 입력하라}} | + +# 3. 롤백 계획 + +## 롤백 트리거 +- 에러율 임계치: 배포 후 15분 이동 평균 기준 5xx 비율 > 2.0% 5분 이상 지속 +- 지연 임계치: p95 응답 지연이 배포 전 기준 대비 50% 이상 상승 상태가 10분 이상 지속 +- 장애 지표: bot token issue/revoke 명령 실패율 > 5% 또는 인증/권한 오류 급증 + +## 롤백 절차 +1. 배포 중단 선언 및 온콜 채널에 롤백 트리거 충족 사실 공유 +2. 직전 안정 커밋으로 애플리케이션 버전 롤백 후 서비스 재기동 +3. 롤백 완료 후 핵심 검증 시나리오와 모니터링 지표 정상화 여부 확인 + +## 데이터 롤백 전략 +- 스키마/데이터 복원 여부: 스키마 변경 없음으로 데이터 롤백 불필요 +- 절차: 애플리케이션 바이너리/코드만 이전 버전으로 복구, 운영 데이터는 유지 + +## 롤백 후 검증 +- 핵심 시나리오 확인 항목: + - healthcheck 200 응답 확인 + - 기존 토큰 인증 경로 정상 동작 확인 + - CLI 기본 조회/쓰기 흐름 회귀 스모크 통과 확인 + +## 롤백 커뮤니케이션 +- 공지 채널: {{DATA: 롤백 공지 채널(예: Slack #oncall, #backend-alerts)을 입력하라}} +- 공지 시각: 롤백 시작 즉시 1차 공지, 복구 완료 후 2차 공지 +- 상태 업데이트 주기: 15분 간격 + +## 참조 런북 +- 롤백 런북: .github/templates/rollback-runbook.template.md + +# 4. 릴리즈 노트 초안 + +## 변경 요약 +- CLI bot token 발급/삭제 명령 추가 및 관련 인증 정책/출력 보안 동작을 반영한다. + +## 주요 변경 내용 +- bot token issue/revoke CLI 명령 제공 및 help 노출 +- --ttl-days/--no-expiry 상호배타 검증, 기본 TTL 365일 정책 적용 +- 비관리자 no-expiry 제한 및 권한 검증 강화 +- 토큰 원문 출력 노출 방지(마스킹) 및 감사 로그 분기 처리 강화 + +## 영향 범위 +- 사용자 영향: 운영자/개발자 CLI 사용 흐름에 bot token 라이프사이클 명령이 추가된다. +- 시스템 영향: 인증/권한 경로와 감사 로그 경로의 실행 분기 증가, API 인터페이스 변경은 없음 + +## 알려진 제한 사항 +- 배포 방식(점진/일괄) 및 운영 공지 채널/온콜 확정 전까지 운영 절차 최종 승인 불가 + +# 5. 배포 후 관찰 계획 + +| 모니터링 항목 | 측정 지표 | 정상 범위 | 이상 감지 기준 | 담당자 | +|---|---|---|---|---| +| API 안정성 | 5xx 비율 | < 1.0% | 2.0% 초과 5분 지속 | {{DATA: 담당자 지정}} | +| API 성능 | p95 지연 | 배포 전 기준 ±20% | 기준 대비 +50% 10분 지속 | {{DATA: 담당자 지정}} | +| CLI 기능 품질 | issue/revoke 성공률 | > 99% | 95% 미만 10분 지속 | {{DATA: 담당자 지정}} | +| 인증/권한 오류 | 401/403 비율 | 배포 전 기준 ±20% | 기준 대비 2배 이상 10분 지속 | {{DATA: 담당자 지정}} | + +## 관찰 기간 +- 집중 관찰 기간: 배포 후 24시간 +- 일반 관찰 기간: 배포 후 7일 + +# 6. 승인된 예외 인계 목록 +해당 없음 + +# 7. Operations 단계 전달 체크 +- [ ] 롤백 가능한 배포 계획 승인됨 +- [ ] 배포 체크리스트 항목 전체 "완료" 또는 "해당없음" 처리됨 +- [x] 릴리즈/롤백 체크리스트가 근거와 함께 완료됨 +- [x] 배포 후 관찰 항목이 정의됨 +- [x] Operations에 전달할 관찰 포인트, 장애 트리거, 후속 대응 절차 준비됨 +- [x] 승인된 예외 이력 인계됨 +- [ ] 최종 승인 + - {{CONFIRM: Release 초안 승인 여부를 확인하라}} + +# 8. 파일 처리 결과 +- 생성/갱신 경로: docs/sdlc/release/rel-001_2026-04-15_cli-bot-token-lifecycle-cli.release.md + +# 9. 사용자 결정 필요 항목 요약 +## {{DECIDE}} +- 이 Release의 최종 승인자는 누구인가? +- 점진 배포/일괄 배포 중 선택하라 + +## {{CONFIRM}} +- 승인 후 기재 +- Release 초안 승인 여부를 확인하라 + +## {{DATA}} +- 배포 대상 환경(dev/staging/prod)과 배포 윈도우를 입력하라 +- 실제 배포 환경의 시크릿/키 점검 결과를 입력하라 +- 공지 채널/온콜 담당자/공지 시점을 입력하라 +- 롤백 공지 채널(예: Slack #oncall, #backend-alerts)을 입력하라 +- 모니터링 담당자를 지정하라