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)