diff --git a/docs/ouroboros/01_requirements_registry.md b/docs/ouroboros/01_requirements_registry.md index 7248955..56cb062 100644 --- a/docs/ouroboros/01_requirements_registry.md +++ b/docs/ouroboros/01_requirements_registry.md @@ -1,9 +1,9 @@ # 요구사항 원장 (Single Source of Truth) diff --git a/docs/ouroboros/30_code_level_work_orders.md b/docs/ouroboros/30_code_level_work_orders.md index 5a75a02..66eb5c9 100644 --- a/docs/ouroboros/30_code_level_work_orders.md +++ b/docs/ouroboros/30_code_level_work_orders.md @@ -16,42 +16,42 @@ Updated: 2026-02-26 ## 구현 단위 A: 상태기계/청산 -- `TASK-CODE-001` (`REQ-V2-001`,`REQ-V2-002`,`REQ-V2-003`): `src/strategy/`에 상태기계 모듈 추가 -- `TASK-CODE-002` (`REQ-V2-004`): ATR/BE/Hard Stop 결합 청산 함수 추가 -- `TASK-CODE-003` (`REQ-V2-008`): Kill Switch 오케스트레이터를 `src/core/kill_switch.py`에 추가 +- `TASK-CODE-001` (`REQ-V2-001`,`REQ-V2-002`,`REQ-V2-003`,`TEST-CODE-001`,`TEST-CODE-002`): `src/strategy/`에 상태기계 모듈 추가 +- `TASK-CODE-002` (`REQ-V2-004`,`TEST-ACC-011`): ATR/BE/Hard Stop 결합 청산 함수 추가 +- `TASK-CODE-003` (`REQ-V2-008`,`TEST-ACC-002`): Kill Switch 오케스트레이터를 `src/core/kill_switch.py`에 추가 - `TEST-CODE-001`: 갭 점프 시 최고상태 승격 테스트 - `TEST-CODE-002`: EXIT 우선순위 테스트 ## 구현 단위 B: 라벨링/검증 -- `TASK-CODE-004` (`REQ-V2-005`): Triple Barrier 라벨러 모듈 추가(`src/analysis/` 또는 `src/strategy/`) -- `TASK-CODE-005` (`REQ-V2-006`): Walk-forward + Purge/Embargo 분할 유틸 추가 -- `TASK-CODE-006` (`REQ-V2-007`): 백테스트 실행기에서 비용/슬리피지 옵션 필수화 +- `TASK-CODE-004` (`REQ-V2-005`,`TEST-CODE-003`,`TEST-ACC-012`): Triple Barrier 라벨러 모듈 추가(`src/analysis/` 또는 `src/strategy/`) +- `TASK-CODE-005` (`REQ-V2-006`,`TEST-CODE-004`,`TEST-ACC-013`): Walk-forward + Purge/Embargo 분할 유틸 추가 +- `TASK-CODE-006` (`REQ-V2-007`,`TEST-ACC-014`): 백테스트 실행기에서 비용/슬리피지 옵션 필수화 - `TEST-CODE-003`: 라벨 선터치 우선 테스트 - `TEST-CODE-004`: 누수 차단 테스트 ## 구현 단위 C: 세션/주문 정책 -- `TASK-CODE-007` (`REQ-V3-001`,`REQ-V3-002`): 세션 분류/전환 훅을 `src/markets/schedule.py` 연동 -- `TASK-CODE-008` (`REQ-V3-003`,`REQ-V3-004`): 블랙아웃 큐 처리기를 `src/broker/`에 추가 -- `TASK-CODE-009` (`REQ-V3-005`): 세션별 주문 타입 검증기 추가 +- `TASK-CODE-007` (`REQ-V3-001`,`REQ-V3-002`,`TEST-ACC-015`,`TEST-ACC-016`): 세션 분류/전환 훅을 `src/markets/schedule.py` 연동 +- `TASK-CODE-008` (`REQ-V3-003`,`REQ-V3-004`,`TEST-CODE-005`,`TEST-ACC-017`): 블랙아웃 큐 처리기를 `src/broker/`에 추가 +- `TASK-CODE-009` (`REQ-V3-005`,`TEST-CODE-006`,`TEST-ACC-004`): 세션별 주문 타입 검증기 추가 - `TEST-CODE-005`: 블랙아웃 신규주문 차단 테스트 - `TEST-CODE-006`: 저유동 세션 시장가 거부 테스트 ## 구현 단위 D: 체결/환율/오버나잇 -- `TASK-CODE-010` (`REQ-V3-006`): 불리한 체결가 모델을 백테스트 체결기로 구현 -- `TASK-CODE-011` (`REQ-V3-007`): FX PnL 분리 회계 테이블/컬럼 추가 -- `TASK-CODE-012` (`REQ-V3-008`): 오버나잇 예외와 Kill Switch 충돌 해소 로직 구현 +- `TASK-CODE-010` (`REQ-V3-006`,`TEST-CODE-007`,`TEST-ACC-005`): 불리한 체결가 모델을 백테스트 체결기로 구현 +- `TASK-CODE-011` (`REQ-V3-007`,`TEST-CODE-008`,`TEST-ACC-006`): FX PnL 분리 회계 테이블/컬럼 추가 +- `TASK-CODE-012` (`REQ-V3-008`,`TEST-ACC-018`): 오버나잇 예외와 Kill Switch 충돌 해소 로직 구현 - `TEST-CODE-007`: 불리한 체결가 모델 테스트 - `TEST-CODE-008`: FX 버퍼 위반 시 신규진입 제한 테스트 ## 구현 단위 E: 운영/문서 거버넌스 -- `TASK-OPS-001` (`REQ-OPS-001`): 시간 필드/로그 스키마의 타임존 표기 강제 규칙 구현 -- `TASK-OPS-002` (`REQ-OPS-002`): 정책 수치 변경 시 `01_requirements_registry.md` 선수정 CI 체크 추가 -- `TASK-OPS-003` (`REQ-OPS-003`): `TASK-*` 없는 `REQ-*` 또는 `TEST-*` 없는 `REQ-*`를 차단하는 문서 검증 게이트 유지 -- `TASK-OPS-004` (`REQ-OPS-004`): v2/v3 원본 계획 문서 위치를 `docs/ouroboros/source/`로 표준화하고 링크 일관성 검증 +- `TASK-OPS-001` (`REQ-OPS-001`,`TEST-ACC-007`): 시간 필드/로그 스키마의 타임존(KST/UTC) 표기 강제 규칙 구현 +- `TASK-OPS-002` (`REQ-OPS-002`,`TEST-ACC-008`): 정책 수치 변경 시 `01_requirements_registry.md` 선수정 CI 체크 추가 +- `TASK-OPS-003` (`REQ-OPS-003`,`TEST-ACC-009`): `TASK-*` 없는 `REQ-*` 또는 `TEST-*` 없는 `REQ-*`를 차단하는 문서 검증 게이트 유지 +- `TASK-OPS-004` (`REQ-OPS-004`,`TEST-ACC-019`): v2/v3 원본 계획 문서 위치를 `docs/ouroboros/source/`로 표준화하고 링크 일관성 검증 ## 커밋 규칙 diff --git a/scripts/validate_governance_assets.py b/scripts/validate_governance_assets.py index 012138a..bfce37c 100644 --- a/scripts/validate_governance_assets.py +++ b/scripts/validate_governance_assets.py @@ -17,6 +17,7 @@ TASK_ID_IN_TEXT = re.compile(r"\bTASK-[A-Z0-9-]+-\d{3}\b") TEST_ID_IN_TEXT = re.compile(r"\bTEST-[A-Z0-9-]+-\d{3}\b") READ_ONLY_FILES = {"src/core/risk_manager.py"} PLACEHOLDER_VALUES = {"", "tbd", "n/a", "na", "none", "", ""} +TIMEZONE_TOKEN_PATTERN = re.compile(r"\b(?:KST|UTC)\b") def must_contain(path: Path, required: list[str], errors: list[str]) -> None: @@ -105,7 +106,43 @@ def validate_task_req_mapping(errors: list[str], *, task_doc: Path | None = None errors.append(f"{path}: no TASK definitions found") -def validate_pr_traceability(warnings: list[str]) -> None: +def validate_task_test_pairing(errors: list[str], *, task_doc: Path | None = None) -> None: + """Fail when TASK definitions are not linked to at least one TEST id.""" + path = task_doc or Path(TASK_WORK_ORDERS_DOC) + if not path.exists(): + errors.append(f"missing file: {path}") + return + + text = path.read_text(encoding="utf-8") + found_task = False + for line in text.splitlines(): + m = TASK_DEF_LINE.match(line.strip()) + if not m: + continue + found_task = True + if not TEST_ID_IN_TEXT.search(m.group("body")): + errors.append(f"{path}: TASK without TEST mapping -> {m.group('task_id')}") + if not found_task: + errors.append(f"{path}: no TASK definitions found") + + +def validate_timezone_policy_tokens(errors: list[str]) -> None: + """Fail-fast check for REQ-OPS-001 governance tokens.""" + required_docs = [ + Path("docs/ouroboros/01_requirements_registry.md"), + Path("docs/ouroboros/30_code_level_work_orders.md"), + Path("docs/workflow.md"), + ] + for path in required_docs: + if not path.exists(): + errors.append(f"missing file: {path}") + continue + text = path.read_text(encoding="utf-8") + if not TIMEZONE_TOKEN_PATTERN.search(text): + errors.append(f"{path}: missing timezone policy token (KST/UTC)") + + +def validate_pr_traceability(errors: list[str]) -> None: title = os.getenv("GOVERNANCE_PR_TITLE", "").strip() body = os.getenv("GOVERNANCE_PR_BODY", "").strip() if not title and not body: @@ -113,11 +150,11 @@ def validate_pr_traceability(warnings: list[str]) -> None: text = f"{title}\n{body}" if not REQ_ID_IN_LINE.search(text): - warnings.append("PR text missing REQ-ID reference") + errors.append("PR text missing REQ-ID reference") if not TASK_ID_IN_TEXT.search(text): - warnings.append("PR text missing TASK-ID reference") + errors.append("PR text missing TASK-ID reference") if not TEST_ID_IN_TEXT.search(text): - warnings.append("PR text missing TEST-ID reference") + errors.append("PR text missing TEST-ID reference") def _parse_pr_evidence_line(text: str, field: str) -> str | None: @@ -145,8 +182,8 @@ def validate_read_only_approval( body = os.getenv("GOVERNANCE_PR_BODY", "").strip() if not body: - warnings.append( - "READ-ONLY file changed but PR body is unavailable; approval evidence check skipped" + errors.append( + "READ-ONLY file changed but PR body is unavailable; approval evidence is required" ) return @@ -245,7 +282,9 @@ def main() -> int: validate_registry_sync(changed_files, errors) validate_task_req_mapping(errors) - validate_pr_traceability(warnings) + validate_task_test_pairing(errors) + validate_timezone_policy_tokens(errors) + validate_pr_traceability(errors) validate_read_only_approval(changed_files, errors, warnings) if errors: diff --git a/tests/test_validate_governance_assets.py b/tests/test_validate_governance_assets.py index f5a425d..30312f5 100644 --- a/tests/test_validate_governance_assets.py +++ b/tests/test_validate_governance_assets.py @@ -108,14 +108,14 @@ def test_validate_task_req_mapping_passes_when_req_present(tmp_path) -> None: assert errors == [] -def test_validate_pr_traceability_warns_when_req_missing(monkeypatch) -> None: +def test_validate_pr_traceability_fails_when_req_missing(monkeypatch) -> None: module = _load_module() monkeypatch.setenv("GOVERNANCE_PR_TITLE", "feat: update policy checker") monkeypatch.setenv("GOVERNANCE_PR_BODY", "Refs: TASK-OPS-001 TEST-ACC-007") - warnings: list[str] = [] - module.validate_pr_traceability(warnings) - assert warnings - assert "PR text missing REQ-ID reference" in warnings + errors: list[str] = [] + module.validate_pr_traceability(errors) + assert errors + assert "PR text missing REQ-ID reference" in errors def test_validate_read_only_approval_requires_evidence(monkeypatch) -> None: @@ -165,7 +165,7 @@ def test_validate_read_only_approval_passes_with_complete_evidence(monkeypatch) assert warnings == [] -def test_validate_read_only_approval_warns_without_pr_body(monkeypatch) -> None: +def test_validate_read_only_approval_fails_without_pr_body(monkeypatch) -> None: module = _load_module() changed_files = ["src/core/risk_manager.py"] errors: list[str] = [] @@ -173,9 +173,9 @@ def test_validate_read_only_approval_warns_without_pr_body(monkeypatch) -> None: monkeypatch.delenv("GOVERNANCE_PR_BODY", raising=False) module.validate_read_only_approval(changed_files, errors, warnings) - assert errors == [] - assert warnings - assert "approval evidence check skipped" in warnings[0] + assert warnings == [] + assert errors + assert "approval evidence is required" in errors[0] def test_validate_read_only_approval_skips_when_no_readonly_file_changed() -> None: @@ -284,3 +284,54 @@ def test_must_contain_fails_when_commands_missing_newline_section_token(tmp_path errors, ) assert any("Comment Newline Escaping" in err for err in errors) + + +def test_validate_task_test_pairing_reports_missing_test_reference(tmp_path) -> None: + module = _load_module() + doc = tmp_path / "work_orders.md" + doc.write_text( + "- `TASK-OPS-999` (`REQ-OPS-001`): enforce timezone labels only\n", + encoding="utf-8", + ) + errors: list[str] = [] + module.validate_task_test_pairing(errors, task_doc=doc) + assert errors + assert "TASK without TEST mapping" in errors[0] + + +def test_validate_task_test_pairing_passes_when_test_present(tmp_path) -> None: + module = _load_module() + doc = tmp_path / "work_orders.md" + doc.write_text( + "- `TASK-OPS-999` (`REQ-OPS-001`,`TEST-ACC-007`): enforce timezone labels\n", + encoding="utf-8", + ) + errors: list[str] = [] + module.validate_task_test_pairing(errors, task_doc=doc) + assert errors == [] + + +def test_validate_timezone_policy_tokens_requires_kst_or_utc(tmp_path, monkeypatch) -> None: + module = _load_module() + docs = tmp_path / "docs" + ouroboros = docs / "ouroboros" + docs.mkdir(parents=True) + ouroboros.mkdir(parents=True) + monkeypatch.chdir(tmp_path) + + (ouroboros / "01_requirements_registry.md").write_text("REQ-OPS-001\nUTC\n", encoding="utf-8") + (ouroboros / "30_code_level_work_orders.md").write_text( + "TASK-OPS-001 (`REQ-OPS-001`,`TEST-ACC-007`)\nKST\n", + encoding="utf-8", + ) + (docs / "workflow.md").write_text("timezone policy: KST and UTC\n", encoding="utf-8") + + errors: list[str] = [] + module.validate_timezone_policy_tokens(errors) + assert errors == [] + + (docs / "workflow.md").write_text("timezone policy missing labels\n", encoding="utf-8") + errors = [] + module.validate_timezone_policy_tokens(errors) + assert errors + assert any("missing timezone policy token" in err for err in errors)