From 6c4a8420dde0b93d1ef1ef8fd4e028edcc94931d Mon Sep 17 00:00:00 2001 From: positivef <107291218+positivef@users.noreply.github.com> Date: Fri, 24 Oct 2025 01:26:19 +0900 Subject: [PATCH] feat: add governance onboarding and roi automation --- README.md | 29 ++++ docs/CONSTITUTION_ONBOARDING_GUIDE.md | 87 ++++++++++ docs/LAYER4_7_IMPLEMENTATION.md | 82 ++++++++++ scripts/governance_gate.py | 167 ++++++++++++++++++++ scripts/roi_report.py | 105 ++++++++++++ templates/governance/p11-p13-checklist.yaml | 42 +++++ 6 files changed, 512 insertions(+) create mode 100644 docs/CONSTITUTION_ONBOARDING_GUIDE.md create mode 100644 docs/LAYER4_7_IMPLEMENTATION.md create mode 100644 scripts/governance_gate.py create mode 100644 scripts/roi_report.py create mode 100644 templates/governance/p11-p13-checklist.yaml diff --git a/README.md b/README.md index 71e5dd29e..d5e42fdf2 100644 --- a/README.md +++ b/README.md @@ -152,11 +152,28 @@ Layer 7: Visualization (Streamlit Dashboard - 시각화만) - `config/constitution.yaml` - 헌법 전문 (800+ 줄) - `NORTH_STAR.md` - 1분 읽기, 방향성 재확인 - `.github/TASK_TEMPLATE.md` - 작업 명세 템플릿 +- `docs/CONSTITUTION_ONBOARDING_GUIDE.md` - 헌법 온보딩 워크숍 로드맵 +- `docs/LAYER4_7_IMPLEMENTATION.md` - Layer 4~7 최적화·시각화 구현 예시 --- ## 🚀 빠른 시작 (5분) +### Step 0: 헌법 온보딩 & 거버넌스 세팅 + +1. `docs/CONSTITUTION_ONBOARDING_GUIDE.md`와 `NORTH_STAR.md`를 읽고 팀 맞춤 체크리스트를 작성합니다. +2. 거버넌스 템플릿 복사 및 편집: + ```bash + mkdir -p RUNS/drafts + cp templates/governance/p11-p13-checklist.yaml RUNS/drafts/DECISION-DEMO.yaml + # 편집 후 검증 및 Evidence 저장 + python scripts/governance_gate.py \ + --checklist RUNS/drafts/DECISION-DEMO.yaml \ + --task-id DEMO-000 \ + --output-dir RUNS/evidence + ``` +3. 거버넌스 Evidence (`RUNS/evidence/DEMO-000/governance.json`)가 생성되었는지 확인합니다. + ### Option 1: Bash Wrapper (권장 - Linux/macOS/Git Bash) `setup.sh`는 내부적으로 `setup.py`를 호출하며, 실패 시 자동 롤백 기능을 포함합니다. @@ -221,6 +238,16 @@ npm run release -- --dry-run # 참고: 루트에 `.nvmrc`(Node 20)를 제공하므로 `nvm use` 혹은 `corepack enable` 환경에서 맞춰 실행하세요. ``` +### Step 4: ROI & 대시보드 초기화 + +```bash +# ROI 리포트 생성 및 최신 JSON 출력 저장 +python scripts/roi_report.py --governance-reviews 1 --compliance-rate 98.5 + +# (선택) Streamlit 대시보드에서 활용 +streamlit run streamlit_app.py +``` + --- ## 📁 디렉토리 구조 @@ -324,6 +351,8 @@ projects: **브레이크이븐**: 3.2개월 **연간 절감**: 264시간 (33일) +> ROI 수치는 `scripts/roi_report.py`를 실행해 팀별 입력값으로 즉시 재계산할 수 있습니다. + --- ## 🆘 문제 해결 diff --git a/docs/CONSTITUTION_ONBOARDING_GUIDE.md b/docs/CONSTITUTION_ONBOARDING_GUIDE.md new file mode 100644 index 000000000..dee45af84 --- /dev/null +++ b/docs/CONSTITUTION_ONBOARDING_GUIDE.md @@ -0,0 +1,87 @@ +# Constitution Onboarding Guide + +> **목적**: `config/constitution.yaml`을 조직 상황에 맞게 빠르게 조정하고, 실행형 자산 시스템을 온보딩할 때 흔히 겪는 시행착오를 줄이기 위한 가이드입니다. + +--- + +## 1. 준비 체크리스트 (30분) + +| 단계 | 설명 | 참고 자료 | +|------|------|-----------| +| 1 | NORTH_STAR.md를 읽어 핵심 철학 복습 | `NORTH_STAR.md` | +| 2 | 현재 조직의 필수 규범 정리 | 내부 표준 문서 | +| 3 | 적용 범위 확정 (팀/프로젝트/PoC) | OKR, 프로젝트 계획 | +| 4 | TaskExecutor 데모 실행 | `scripts/demo_executor.py` | + +> **TIP**: 첫 워크숍에서는 `config/constitution.yaml` 전체를 다루지 말고, 반드시 필요한 조항(P1~P10)만 골라 적용 범위를 정하세요. + +--- + +## 2. 조항 맞춤화 절차 (Half-day Workshop) + +1. **핵심 조항 선별** + - 조직 필수 규범을 P1~P10과 매핑합니다. + - 겹치는 항목은 기존 조항에 설명을 추가하고, 없는 항목만 별도 조항으로 작성합니다. +2. **거버넌스 조항(P11~P13) 시뮬레이션** + - 최근 결정 사례 1~2개를 선택해 P11~P13 체크리스트를 적용해봅니다. + - 누락된 승인 단계나 기록 포인트가 있는지 확인합니다. +3. **체크리스트 작성** + - `templates/governance/p11-p13-checklist.yaml`을 복제해 팀 상황에 맞게 수정합니다. + - TaskExecutor 실행 전에 체크리스트를 채우도록 표준 운영 절차(SOP)에 포함합니다. +4. **증거 경로 설정** + - `RUNS/evidence/` 하위에 팀별 폴더 구조를 합의하고, ObsidianVault 경로를 설정합니다. + +> **워크숍 산출물**: 수정된 `constitution.yaml`, 맞춤형 거버넌스 체크리스트, 증거 수집 경로 정의 문서. + +--- + +## 3. 역할별 온보딩 가이드 + +### 헌법 관리자 (Lead) +- Constitution 변경 Pull Request 리뷰 책임. +- `scripts/governance_gate.py`를 실행해 체크리스트가 최신 상태인지 확인합니다. +- 분기별로 ROI 리포트를 생성해 경영진에 공유합니다. + +### TaskExecutor 운영자 +- 작업 시작 전 `templates/governance/p11-p13-checklist.yaml` 작성 상태를 확인합니다. +- `scripts/enhanced_task_executor.py` 실행 시 `--force` 플래그 사용을 최소화하고, 경고 로그를 Evidence에 저장합니다. + +### 의사결정 승인자 +- P11~P13 문항 중 *승인* 항목에 대한 최종 결정을 내립니다. +- 승인 기록은 `RUNS/evidence//governance.json`에 자동 저장되는지 점검합니다. + +--- + +## 4. 30일 운영 로드맵 + +| 주차 | 목표 | 실행 포인트 | +|------|------|-------------| +| 1주차 | 파일 구조 정리 및 TaskExecutor 데모 | 샘플 YAML 계약서 실행, Evidence 자동 수집 확인 | +| 2주차 | Constitution 맞춤화 초안 완성 | 워크숍 진행, 거버넌스 체크리스트 초안 작성 | +| 3주차 | 거버넌스 자동화 파일 연결 | `scripts/governance_gate.py` 통합, Evidence 저장 경로 검증 | +| 4주차 | ROI 대시보드 초안 발표 | `scripts/roi_report.py`로 KPI 생성, Streamlit 보조 화면 구성 | + +--- + +## 5. Troubleshooting FAQ + +**Q1. 조항이 너무 많아서 팀이 부담스러워합니다.** +A. `config/constitution.yaml`에서 필수 조항만 남긴 *Lite 버전*을 만들어 첫 달에 사용하세요. 나머지는 체크리스트를 통해 단계적으로 도입합니다. + +**Q2. 증거 저장이 누락됩니다.** +A. `scripts/governance_gate.py` 실행 시 Evidence 경로 유효성을 검증합니다. 경로가 없으면 자동 생성하고, README에 기록하십시오. + +**Q3. ROI가 체감되지 않습니다.** +A. `scripts/roi_report.py`의 입력값 중 "절약 시간"을 실제 팀 데이터를 기반으로 업데이트하세요. Streamlit 대시보드에는 최대 3개의 KPI만 노출하여 집중도를 높입니다. + +--- + +## 6. 다음 단계 + +1. `docs/LAYER4_7_IMPLEMENTATION.md`를 읽고 최적화~시각화 계층의 구현 예시를 검토합니다. +2. `scripts/roi_report.py`를 실행하여 첫 번째 ROI 리포트를 생성하고, 경영진 브리핑 자료에 포함합니다. +3. 헌법 변경이 필요한 경우 `templates/governance/p11-p13-checklist.yaml`을 통해 P13 절차를 수행한 뒤 Pull Request를 올립니다. + +--- + +**최종 목표**: 모든 의사결정과 실행 자료가 Constitution 조항과 Evidence로 연결되어, 새로운 팀원이 합류해도 즉시 동일한 실행형 자산 시스템을 재현할 수 있도록 하는 것입니다. diff --git a/docs/LAYER4_7_IMPLEMENTATION.md b/docs/LAYER4_7_IMPLEMENTATION.md new file mode 100644 index 000000000..fdea4d532 --- /dev/null +++ b/docs/LAYER4_7_IMPLEMENTATION.md @@ -0,0 +1,82 @@ +# Layer 4~7 Implementation Playbook + +> **목적**: Optimization → Evidence → Knowledge Asset → Visualization 계층을 빠르게 체험할 수 있는 최소 구현 사례를 제공합니다. + +--- + +## 1. Layer 4 - Optimization + +### 1.1 VerificationCache 활성화 +- 실행: `python scripts/verification_cache.py --build` (기본 캐시 생성) +- YAML 계약서 내 `use_verification_cache: true` 옵션을 추가하여 재실행 시 중복 검증을 건너뜁니다. +- Evidence: `RUNS//cache/`에 해시 정보가 저장됩니다. + +### 1.2 CriticalFileDetector 적용 +- 실행: `python scripts/critical_file_detector.py --path src/ --top 5` +- 출력: 변경 위험도가 높은 파일 목록(JSON + Markdown) +- 활용: P4, P5 조항 위반 가능성이 높은 파일에 우선적으로 리뷰 리소스를 배정합니다. + +--- + +## 2. Layer 5 - Evidence Collection + +| 작업 | 자동화 포인트 | Evidence 위치 | +|------|---------------|----------------| +| TaskExecutor 실행 | `AutomaticEvidenceTracker`가 실행 로그 및 해시 기록 | `RUNS/evidence//execution.json` | +| 거버넌스 체크리스트 | `scripts/governance_gate.py`가 체크리스트와 승인자를 수집 | `RUNS/evidence//governance.json` | +| 모델 출력 기록 | `AutomaticEvidenceTracker`의 `model_outputs` 옵션 | `RUNS/evidence//models/` | + +> **팁**: Evidence 폴더 구조를 README에 정의하면 신규 팀원 온보딩 속도가 빨라집니다. + +--- + +## 3. Layer 6 - Knowledge Asset + +1. `scripts/obsidian_bridge.py --vault ~/Obsidian/ExecutableAssets` +2. 위 명령을 TaskExecutor 후크로 등록하면 Evidence가 Obsidian Vault에 동기화됩니다. +3. Vault 구조 예시: + ``` + Vault/ + 01_Constitution/ + 02_Tasks/ + 03_Evidence/ + 2024-ROI-Report.md + P11-Conflict-Review.md + ``` +4. Obsidian에서 Dataview 플러그인을 사용하면 Evidence 메타데이터를 표 형태로 조회할 수 있습니다. + +--- + +## 4. Layer 7 - Visualization (Streamlit) + +### 4.1 필수 KPI 스크립트 연동 +- `scripts/roi_report.py`의 출력(JSON)을 Streamlit에서 읽어 차트로 표시합니다. +- 최소 KPI: `time_saved_hours`, `constitution_compliance_rate`, `governance_reviews_completed`. + +### 4.2 대시보드 최소 레이아웃 예시 +```python +import json +import streamlit as st + +with open("RUNS/reports/latest_roi.json", "r", encoding="utf-8") as f: + data = json.load(f) + +st.title("Constitution Execution Dashboard") +st.metric("연간 절약 시간", f"{data['time_saved_hours']}시간") +st.metric("헌법 준수율", f"{data['constitution_compliance_rate']}%") +st.metric("거버넌스 리뷰 수", data['governance_reviews_completed']) +``` + +--- + +## 5. 운영 체크리스트 + +| 주기 | 항목 | 담당 | +|------|------|------| +| 매 실행 | `scripts/governance_gate.py` 실행 후 TaskExecutor 시작 | 운영자 | +| 주간 | VerificationCache 재생성 및 CriticalFileDetector 리포트 검토 | 테크 리드 | +| 월간 | `scripts/roi_report.py` 실행, Streamlit 스냅샷 업데이트 | 헌법 관리자 | + +--- + +**다음 단계**: Streamlit 대시보드를 운영하기 전에 ROI 리포트와 Evidence 폴더 구조가 안정화되었는지 확인하고, 필요 시 `docs/CONSTITUTION_ONBOARDING_GUIDE.md`에 있는 워크숍을 반복하세요. diff --git a/scripts/governance_gate.py b/scripts/governance_gate.py new file mode 100644 index 000000000..0eb866302 --- /dev/null +++ b/scripts/governance_gate.py @@ -0,0 +1,167 @@ +"""Governance Gate + +P11~P13 거버넌스 체크리스트를 검증하고 Evidence 저장 경로에 기록합니다. + +Usage: + python scripts/governance_gate.py --checklist path/to/checklist.yaml --task-id FEAT-123 --output-dir RUNS/evidence +""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Tuple + +import yaml + + +PLACEHOLDER_VALUES = {"", "TBD", "TODO"} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Validate governance checklist and persist evidence") + parser.add_argument("--checklist", type=Path, required=True, help="Path to the completed P11~P13 checklist YAML") + parser.add_argument("--task-id", type=str, default=None, help="Task identifier used for evidence storage") + parser.add_argument( + "--output-dir", + type=Path, + default=Path("RUNS/evidence"), + help="Base directory for evidence output (default: RUNS/evidence)", + ) + parser.add_argument( + "--fail-on-warning", + action="store_true", + help="Treat warnings as errors to block execution when required fields are missing", + ) + return parser.parse_args() + + +def load_checklist(path: Path) -> Dict[str, Any]: + if not path.exists(): + raise FileNotFoundError(f"Checklist file not found: {path}") + with path.open("r", encoding="utf-8") as f: + data = yaml.safe_load(f) + if not isinstance(data, dict): + raise ValueError("Checklist must be a YAML mapping") + return data + + +def is_placeholder(value: Any) -> bool: + if value is None: + return True + if isinstance(value, str): + return value.strip() in PLACEHOLDER_VALUES + if isinstance(value, list): + return all(is_placeholder(item) for item in value) + if isinstance(value, dict): + return all(is_placeholder(v) for v in value.values()) + return False + + +def ensure_fields(data: Dict[str, Any]) -> Tuple[List[str], List[str]]: + """Return (errors, warnings) detected in the checklist.""" + + errors: List[str] = [] + warnings: List[str] = [] + + meta = data.get("meta", {}) + for key in ["decision_title", "prepared_by", "date"]: + if is_placeholder(meta.get(key)): + errors.append(f"meta.{key} is missing") + + reviewers = meta.get("reviewers") + if not reviewers or is_placeholder(reviewers): + warnings.append("meta.reviewers should list at least one reviewer") + + p11 = data.get("p11_conflict_review", {}) + if is_placeholder(p11.get("conflict_summary")): + errors.append("p11_conflict_review.conflict_summary is missing") + if is_placeholder(p11.get("resolution")): + errors.append("p11_conflict_review.resolution is missing") + if is_placeholder(p11.get("related_principles")): + warnings.append("p11_conflict_review.related_principles should reference at least one article") + if is_placeholder(p11.get("past_decisions_consulted")): + warnings.append("p11_conflict_review.past_decisions_consulted should cite prior work") + + p12 = data.get("p12_tradeoff_analysis", {}) + options = p12.get("options") or [] + cleaned_options = [opt for opt in options if not is_placeholder(opt.get("name"))] + if not cleaned_options: + errors.append("p12_tradeoff_analysis.options must describe at least one option") + for index, option in enumerate(cleaned_options, start=1): + if is_placeholder(option.get("pros")): + warnings.append(f"p12_tradeoff_analysis.options[{index}].pros is empty") + if is_placeholder(option.get("cons")): + warnings.append(f"p12_tradeoff_analysis.options[{index}].cons is empty") + if is_placeholder(option.get("evidence")): + warnings.append(f"p12_tradeoff_analysis.options[{index}].evidence is empty") + if is_placeholder(p12.get("recommended_option")): + errors.append("p12_tradeoff_analysis.recommended_option is missing") + if is_placeholder(p12.get("rationale")): + errors.append("p12_tradeoff_analysis.rationale is missing") + + p13 = data.get("p13_constitution_change", {}) + if p13.get("required"): + if is_placeholder(p13.get("target_articles")): + errors.append("p13_constitution_change.target_articles is missing") + if is_placeholder(p13.get("change_summary")): + errors.append("p13_constitution_change.change_summary is missing") + if is_placeholder(p13.get("approval_status")): + errors.append("p13_constitution_change.approval_status is missing") + else: + if not p13: + warnings.append("p13_constitution_change section is missing; set required=false when no change is needed") + + signoff = data.get("signoff", {}) + if is_placeholder(signoff.get("approver")): + errors.append("signoff.approver is missing") + if is_placeholder(signoff.get("approval_date")): + warnings.append("signoff.approval_date is missing") + + return errors, warnings + + +def persist_evidence(data: Dict[str, Any], output_dir: Path, task_id: str | None) -> Path: + timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + if task_id: + evidence_dir = output_dir / task_id + evidence_dir.mkdir(parents=True, exist_ok=True) + output_path = evidence_dir / "governance.json" + else: + reports_dir = output_dir / "reports" + reports_dir.mkdir(parents=True, exist_ok=True) + output_path = reports_dir / f"governance_{timestamp}.json" + + payload = { + "generated_at": timestamp, + "task_id": task_id, + "checklist": data, + } + with output_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2, ensure_ascii=False) + return output_path + + +def main() -> None: + args = parse_args() + checklist = load_checklist(args.checklist) + errors, warnings = ensure_fields(checklist) + + if errors: + for msg in errors: + print(f"[ERROR] {msg}") + if warnings: + for msg in warnings: + print(f"[WARN] {msg}") + + if errors or (warnings and args.fail_on_warning): + raise SystemExit("Governance gate validation failed") + + output_path = persist_evidence(checklist, args.output_dir, args.task_id) + print(f"[OK] Governance evidence stored at {output_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/roi_report.py b/scripts/roi_report.py new file mode 100644 index 000000000..cc0dd186c --- /dev/null +++ b/scripts/roi_report.py @@ -0,0 +1,105 @@ +"""ROI Report Generator + +헌법 기반 실행형 자산 시스템의 핵심 KPI를 계산하여 JSON/Markdown Evidence로 저장합니다. + +Usage: + python scripts/roi_report.py --time-invested-hours 264 --time-saved-hours 264 --hourly-rate 80 +""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime +from pathlib import Path +from typing import Dict + +DEFAULT_HOURLY_RATE = 80 # USD 또는 현지 통화 환산 기준 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate ROI metrics for the Development Rules Starter Kit") + parser.add_argument("--time-invested-hours", type=float, default=264.0, help="초기 구축에 투입된 총 시간") + parser.add_argument("--time-saved-hours", type=float, default=264.0, help="연간 절약 가능한 시간") + parser.add_argument("--hourly-rate", type=float, default=DEFAULT_HOURLY_RATE, help="평균 시간당 비용") + parser.add_argument("--governance-reviews", type=int, default=0, help="기간 내 완료한 거버넌스 리뷰 수") + parser.add_argument( + "--compliance-rate", + type=float, + default=97.5, + help="헌법 조항 준수율(%)", + ) + parser.add_argument( + "--output-dir", + type=Path, + default=Path("RUNS/reports"), + help="ROI 리포트를 저장할 경로 (기본: RUNS/reports)", + ) + return parser.parse_args() + + +def compute_roi(time_invested: float, time_saved: float, hourly_rate: float) -> Dict[str, float]: + investment_cost = time_invested * hourly_rate + annual_savings = time_saved * hourly_rate + if investment_cost == 0: + roi_percent = 0.0 + else: + roi_percent = ((annual_savings - investment_cost) / investment_cost) * 100 + + multi_year = { + "year_1": roi_percent, + "year_2": ((annual_savings * 2 - investment_cost) / investment_cost) * 100 if investment_cost else 0.0, + "year_5": ((annual_savings * 5 - investment_cost) / investment_cost) * 100 if investment_cost else 0.0, + } + + return { + "investment_cost": round(investment_cost, 2), + "annual_savings": round(annual_savings, 2), + "roi_percent_year_1": round(multi_year["year_1"], 2), + "roi_percent_year_2": round(multi_year["year_2"], 2), + "roi_percent_year_5": round(multi_year["year_5"], 2), + } + + +def persist_report(data: Dict[str, float], args: argparse.Namespace) -> Path: + args.output_dir.mkdir(parents=True, exist_ok=True) + timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") + payload = { + "generated_at": timestamp, + "time_invested_hours": args.time_invested_hours, + "time_saved_hours": args.time_saved_hours, + "hourly_rate": args.hourly_rate, + "governance_reviews_completed": args.governance_reviews, + "constitution_compliance_rate": args.compliance_rate, + "time_saved_value": round(args.time_saved_hours * args.hourly_rate, 2), + } + payload.update(data) + + json_path = args.output_dir / "latest_roi.json" + with json_path.open("w", encoding="utf-8") as f: + json.dump(payload, f, indent=2, ensure_ascii=False) + + markdown_path = args.output_dir / f"roi_{timestamp}.md" + with markdown_path.open("w", encoding="utf-8") as f: + f.write("# ROI Report\n\n") + f.write(f"- 생성 시각: {timestamp} UTC\n") + f.write(f"- 연간 절약 시간: {args.time_saved_hours}시간\n") + f.write(f"- 헌법 준수율: {args.compliance_rate}%\n") + f.write(f"- 거버넌스 리뷰 수: {args.governance_reviews}\n") + f.write(f"- 연간 비용 절감액: {payload['annual_savings']}\n") + f.write(f"- 1년 ROI: {payload['roi_percent_year_1']}%\n") + f.write(f"- 2년 ROI: {payload['roi_percent_year_2']}%\n") + f.write(f"- 5년 ROI: {payload['roi_percent_year_5']}%\n") + + return json_path + + +def main() -> None: + args = parse_args() + metrics = compute_roi(args.time_invested_hours, args.time_saved_hours, args.hourly_rate) + output = persist_report(metrics, args) + print(f"[OK] ROI report generated at {output}") + + +if __name__ == "__main__": + main() diff --git a/templates/governance/p11-p13-checklist.yaml b/templates/governance/p11-p13-checklist.yaml new file mode 100644 index 000000000..987b2206d --- /dev/null +++ b/templates/governance/p11-p13-checklist.yaml @@ -0,0 +1,42 @@ +# 거버넌스 체크리스트 (P11~P13) +# 사용법: 의사결정 전에 각 항목을 작성하고, `scripts/governance_gate.py`로 검증/저장하세요. + +meta: + decision_title: "" + prepared_by: "" + reviewers: + - "" + date: "" + +p11_conflict_review: + related_principles: + - "P1" + past_decisions_consulted: + - "" + conflict_summary: "" + resolution: "" + +p12_tradeoff_analysis: + options: + - name: "" + pros: + - "" + cons: + - "" + evidence: + - "" + recommended_option: "" + rationale: "" + +p13_constitution_change: + required: false + target_articles: + - "" + change_summary: "" + approval_status: "pending" # pending | approved | rejected + approval_notes: "" + +signoff: + approver: "" + approval_date: "" + additional_notes: ""