# Callback Lifecycle State Schema (2-Axis) — 단일소스 스펙/독트린

- **doc_id**: system_callback_lifecycle_state_schema_260522
- **status**: DRAFT (Phase 1 schema/spec/doctrine single source) · 회장 결정 2026-05-22 채택
- **authored_by**: ANU (설계 단일소스 · 코드 0 — Phase 2 코드화는 task-2627 callback enforcement layer 동반)
- **supersedes / extends**: task-2627 §10 callback contract 9 fields (대체 아님 · 확장)
- **doctrine_links**: feedback_callback_self_key_helper_not_wired_260521 · feedback_progress_trigger_on_normal_callback_not_deadman_260518 · feedback_executor_completion_callback_mandatory_260518 · feedback_no_dual_purpose_watcher_260520 · project_no_bot_app_token_local_only_completion_260521 · feedback_finish_task_shared_branch_gate_misfire_260521

---

## 0. 목적 (회장 verbatim 박제)

> callback이 안 온 경우를 하나로 뭉개지 말고, 원인을 분류하는 runtime/state schema를 만들라.

normal callback 미수신을 단일 사건으로 뭉개면 (a) 진행 트리거 오판 (b) 책임 귀속 오류 (c) DELIVERY_GAP 남용 (회장 반복 정정 지점)이 재발한다. 본 스키마는 callback lifecycle 상태를 **evidence-derivable 한 2축(+root_cause 태그)** 으로 정규화한다.

핵심 구분(회장 필수 반영):
- callback **gate PASS ≠ callback fired**
- ANU **notification sent ≠ ANU collector received**
- **ENVELOPE_PREPARED_NOT_FIRED ≠ normal callback received**
- **fallback collector applied ≠ normal callback success**
- **self-key fail-closed = 안전 차단**(사고 아님)이되 normal callback 미수신 원인으로 **기록**
- **finish-task GIT-GATE 차단 = BEFORE_CALLBACK blocker**(delivery gap 아님)

---

## 1. 왜 flat enum 이 아니라 2축인가 (실증)

flat 단일 enum 은 한 task 가 동시에 여러 값에 해당하는 문제가 있다. **실증(task-2628+1)**: 한 건이 동시에
- ENVELOPE_PREPARED_NOT_FIRED (원인)
- SELF_KEY_FAIL_CLOSED_BEFORE_FIRE (원인 세부)
- FALLBACK_COLLECTOR_APPLIED (수습 결과)

3개 값에 해당 → flat 이면 "어느 값으로 기록?" 모호. 차원이 다른 두 질문("최종 어떻게 수집됐나" vs "왜 normal 이 안 떴나")을 분리해야 한다. → **축A(결과) · 축B(원인) · 축C(root_cause 태그)**.

---

## 2. 축A — `delivery_outcome` (최종 어떻게 수집됐나)

| 값 | 정의 | terminal? |
|---|---|---|
| NORMAL_CALLBACK_RECEIVED | ANU key cron 등록+발사 → ANU collector 세션 spawn → result ack. 정상 경로 | ✅ success |
| FALLBACK_COLLECTOR_APPLIED | normal 미수집 + ANU-key fallback safety-net 발사 → ANU 독립 수집 artifact 생성 | ✅ recovered |
| DUPLICATE_FALLBACK_NO_OP | fallback 발사됐으나 normal 이 이미 수집 완료 → NO_OP·no ledger append (applied_count>1 dedupe) | ✅ no-op |
| MANUAL_ANU_REVERIFY | fallback 미등록/미발사 상태에서 ANU 가 직접(인라인) 독립 재검증으로 수집 | ✅ recovered(manual) |
| NOT_YET_COLLECTED | 아직 어떤 경로로도 authoritative 수집 안 됨 (대기/미수습) | ⏳ pending |

> 표는 마크다운 표 금지 규칙(보고용)과 무관 — 본 문서는 스펙. 보고 시엔 산문화.

---

## 3. 축B — `normal_callback_miss_cause` (왜 normal 이 안 떴나)

- **NONE** — normal callback 정상 수신(축A=NORMAL_CALLBACK_RECEIVED). 미수신 원인 없음.
- **ENVELOPE_PREPARED_NOT_FIRED** — envelope 까지 준비됐으나 발사 안 됨(umbrella). 구체 사유는 축C root_cause 로 분해(self-key fail-closed / token 부재 / reflection 미머지 / reflection 금지 등).
- **FINISH_TASK_GIT_GATE_BLOCKED_BEFORE_CALLBACK** — finish-task GIT-GATE 가 callback 단계 **이전**에 차단. envelope 도달 못 함. (delivery gap 아님)
- **SELF_KEY_FAIL_CLOSED_BEFORE_FIRE** — owner key 가 executor self-key 라 발사 직전 fail-closed(argv=None). **안전 차단**(사고 아님). 단독 주 원인일 때 사용. (token/reflection 등과 공존 시 umbrella=ENVELOPE_PREPARED_NOT_FIRED + root_cause 태그로 격하 — §3.1)
- **SELF_KEY_FIRED_NON_AUTHORITATIVE** — ★ self-key 로 **실제 발사돼** executor 가 자가수집(non-authoritative self-collection). **incident**. task-2625형. SELF_KEY_FAIL_CLOSED_BEFORE_FIRE 와 정반대(차단 실패 → 발사됨).
- **CALLBACK_DELIVERY_GAP** — ★ **residual 전용**. ANU key 로 등록+발사까지 됐는데 collector 가 안 뜬 경우 + GIT-GATE/self-key/envelope-not-fired 등 BEFORE_FIRE 원인이 **아무것도 없을 때만**. (과거 남용 차단 — 회장 정정 박제)
- **CALLBACK_CONTRACT_VIOLATION** — result 존재하나 필수 callback 필드 누락/위반(envelope>3900 bytes, 9 fields 누락, role 오류 등).

### 3.1 SELF_KEY_FAIL_CLOSED vs ENVELOPE_PREPARED_NOT_FIRED 우선순위
self-key fail-closed 가 **유일** 사유면 축B=SELF_KEY_FAIL_CLOSED_BEFORE_FIRE.
self-key fail-closed 가 token 부재·reflection 미머지·reflection 금지 등과 **공존**하면 축B=ENVELOPE_PREPARED_NOT_FIRED(umbrella) + 축C 태그(SELF_KEY_FAIL_CLOSED_BEFORE_FIRE 포함). (task-2628+1 이 후자 — 회장 매핑과 일치)

---

## 4. 축C — `root_cause_tags` (evidence-derivable 다중 태그)

- **FOREIGN_DIRTY_BLOCKER** — 공유 workspace 의 본 task 무관 uncommitted dirty 가 GIT-GATE 차단 유발
- **BOT_APP_TOKEN_ABSENT** — ghs_ App token 부재 → 봇 자율 발사/push 불가 (ghp_ OWNER PAT only)
- **REFLECTION_NOT_MERGED** — callback enforcement(task-2627) 가 origin/main 미반영 → self-key 차단 런타임 미결선
- **GIT_GATE_SHARED_WORKSPACE_MISFIRE** — finish-task GIT-GATE 가 worktree 가 아닌 메인 workspace status 를 검사하는 오발화
- **SCOPE_GUARD_MAIN_HEAD_DIVERGENCE** — scope-guard 가 per-task diff 아닌 main..HEAD divergence 로 오탐
- **CALLBACK_ENVELOPE_ONLY** — envelope-only 정책상 본문 미포함(정상 제약 — 위반 아님, 관측 태그)
- 기타: evidence-derivable 하면 추가 가능(append-only · 임의 추론 금지)

---

## 5. 상태기계 (전이표 · 각 전이 실패점 = 원인)

```
[S0 task_complete]
   │ git/scope/gate (finish-task GIT-GATE, scope-guard)
   ▼  ──fail──▶ B:FINISH_TASK_GIT_GATE_BLOCKED_BEFORE_CALLBACK  (C:FOREIGN_DIRTY_BLOCKER / GIT_GATE_SHARED_WORKSPACE_MISFIRE / SCOPE_GUARD_MAIN_HEAD_DIVERGENCE)
[S1 gate_passed]
   │ build envelope
   ▼  ──fail(fields)──▶ B:CALLBACK_CONTRACT_VIOLATION
[S2 envelope_prepared]
   │ owner key check (ANU vs self)
   ├─ self & fail-closed ──▶ B:SELF_KEY_FAIL_CLOSED_BEFORE_FIRE  (또는 umbrella ENVELOPE_PREPARED_NOT_FIRED + 태그 — §3.1)
   ├─ self & fired ───────▶ B:SELF_KEY_FIRED_NON_AUTHORITATIVE  (incident)
   │ not fired (token 부재/reflection 미머지/reflection 금지)
   ▼  ──not fired──▶ B:ENVELOPE_PREPARED_NOT_FIRED  (C:BOT_APP_TOKEN_ABSENT / REFLECTION_NOT_MERGED)
[S3 cron_registered(ANU key)]
   │ fire
   ▼
[S4 cron_fired]
   │ collector spawn
   ▼  ──fail(no collector, ANU key, no BEFORE_FIRE cause)──▶ B:CALLBACK_DELIVERY_GAP  (residual)
[S5 collector_spawned]
   │ receive/apply
   ▼
[S6 collector_received] ──▶ A:NORMAL_CALLBACK_RECEIVED (B:NONE)

[복구 경로 — normal 미도달 시]
   fallback ANU-key cron 발사 → 수집 ──▶ A:FALLBACK_COLLECTOR_APPLIED
   fallback 발사했으나 normal 이미 수집 ──▶ A:DUPLICATE_FALLBACK_NO_OP
   fallback 미등록·ANU 인라인 재검증 ──▶ A:MANUAL_ANU_REVERIFY
   아직 미수습 ──▶ A:NOT_YET_COLLECTED
```

**precedence**: 축B 는 상태기계상 **가장 이른 차단 전이**가 primary cause (이후 전이는 도달 못 했으므로 moot). 공존 사유는 축C 태그로.

---

## 6. Evidence derivation rule (결정적 자동 판정 — 추정 금지)

각 상태는 아래 관측 소스로 **자동 판정 가능**해야 한다(회장 doctrine: cron-history/schedule_history 로 판정, 추정 금지).

판정 소스:
- `result.json.callback_registration_status` (ENVELOPE_PREPARED_NOT_FIRED / FIRED / …)
- `schedule_history/<cron_id>.log` + cron **owner key** (c119085a=ANU vs 1e41a232=self) — 발사 주체·발사 여부
- finish-task gate log / `escalate.reason` (finish_task_git_gate_blocked_by_foreign_stale_dirty 등)
- collector artifact 존재: `<task>.independent-anu-collector.result.json`(fallback/manual) · normal 수집 레코드
- `applied_count` / `callback_ack` (dedupe → DUPLICATE_FALLBACK_NO_OP)
- `git status` / foreign dirty evidence (root_cause 태그)

판정 의사코드(축A → 축B → 축C 순):
```
# 축A delivery_outcome
if normal_collection(ANU-key cron fired ∧ collector spawn ∧ ack):      A=NORMAL_CALLBACK_RECEIVED
elif fallback_artifact ∧ ¬normal_collection ∧ applied_count==1:        A=FALLBACK_COLLECTOR_APPLIED
elif fallback_fired ∧ normal_collection(applied_count>1):              A=DUPLICATE_FALLBACK_NO_OP
elif anu_manual_reverify_artifact ∧ ¬cron_collection:                 A=MANUAL_ANU_REVERIFY
else:                                                                  A=NOT_YET_COLLECTED

# 축B (A==NORMAL_CALLBACK_RECEIVED → NONE)
if A==NORMAL_CALLBACK_RECEIVED:                                        B=NONE
elif escalate.reason ~ git_gate_blocked (envelope 미도달):             B=FINISH_TASK_GIT_GATE_BLOCKED_BEFORE_CALLBACK
elif cron-history: self-key cron FIRED (argv≠None):                    B=SELF_KEY_FIRED_NON_AUTHORITATIVE   # incident
elif registration_status==ENVELOPE_PREPARED_NOT_FIRED ∧ 다중 BEFORE_FIRE 사유: B=ENVELOPE_PREPARED_NOT_FIRED
elif self-key fail-closed (argv=None) 단독:                            B=SELF_KEY_FAIL_CLOSED_BEFORE_FIRE
elif callback 필드 누락/위반:                                          B=CALLBACK_CONTRACT_VIOLATION
elif ANU-key 등록+발사 ∧ ¬collector ∧ ¬(BEFORE_FIRE 원인):            B=CALLBACK_DELIVERY_GAP   # residual only
else:                                                                  B=ENVELOPE_PREPARED_NOT_FIRED  # 보수적 기본

# 축C root_cause_tags (다중 · evidence 있을 때만 append)
foreign_dirty → FOREIGN_DIRTY_BLOCKER
no ghs_ token → BOT_APP_TOKEN_ABSENT
task-2627 origin/main 부재 → REFLECTION_NOT_MERGED
git-gate checked main≠worktree → GIT_GATE_SHARED_WORKSPACE_MISFIRE
scope-guard main..HEAD → SCOPE_GUARD_MAIN_HEAD_DIVERGENCE
envelope-only 강제 → CALLBACK_ENVELOPE_ONLY
```

**필수**: normal 미수신이라도 fallback collector applied 면 별도 artifact (`<task>.independent-anu-collector.result.json` 또는 `<task>.fallback_collector_applied.json`) 를 남긴다.

---

## 7. 사례 매핑 (회장 명시 3건 + 증거)

**task-2625** (incident)
- 축A: NOT_YET_COLLECTED → 후속 MANUAL_ANU_REVERIFY (self-key 수집은 non-authoritative 이므로 authoritative 수집 0)
- 축B: **SELF_KEY_FIRED_NON_AUTHORITATIVE**
- 축C: REFLECTION_NOT_MERGED
- 증거: cron D4CE6C8D owner key=1e41a232(self) 발사, 본작업 종료 4초 후 executor 자가수집. classification=incident.

**task-2628**
- 축A: **MANUAL_ANU_REVERIFY** (fallback 미등록 → ANU 인라인 독립 재검증)
- 축B: **FINISH_TASK_GIT_GATE_BLOCKED_BEFORE_CALLBACK**
- 축C: **FOREIGN_DIRTY_BLOCKER** (+ GIT_GATE_SHARED_WORKSPACE_MISFIRE)
- 증거: escalate.reason=finish_task_git_gate_blocked_by_foreign_stale_dirty · result.json callback_registration_status=ENVELOPE_PREPARED_NOT_FIRED · foreign dirty 5건.

**task-2628+1**
- 축A: **FALLBACK_COLLECTOR_APPLIED** (cron CEFB90DE 10:15 발사 → independent-anu-collector.result.json 10:17, normal_callback_received=False, no_op=False)
- 축B: **ENVELOPE_PREPARED_NOT_FIRED** (umbrella)
- 축C: **SELF_KEY_FAIL_CLOSED_BEFORE_FIRE / BOT_APP_TOKEN_ABSENT / REFLECTION_NOT_MERGED**
- 증거: result.json not_fired_reasons = [reflection 금지, self-key≠ANU fail-closed argv=None, app token 부재] · fallback collector artifact 존재.

세 건 모두 (축A × 축B) 조합이 상이 → 2축 모델 타당성 실증. task-2625 는 신규 enum(SELF_KEY_FIRED_NON_AUTHORITATIVE) 없으면 매핑 불가.

---

## 8. 기존 callback contract 9 fields 와의 확장 관계 (대체 아님)

기존 9 fields(task-2627 §10) = **per-callback 등록/형식 계약**. 본 스키마 = **lifecycle 결과/원인 분류**. 확장 신규 fields:
- (10) `delivery_outcome` (축A enum)
- (11) `normal_callback_miss_cause` (축B enum)
- (12) `root_cause_tags` (축C list)
- (13) `lifecycle_state_evidence` (판정 근거 소스 dict — registration_status/cron_owner_key/escalate_reason/collector_artifact/applied_count/foreign_dirty)
- (14) `classified_by` ("auto-classifier" / "anu-manual") · `applied_count`

기존 #4 callback_registration_status 는 (11) 의 **입력 신호**로 재활용(중복 taxonomy 신설 금지 — 단일소스). 9 fields 는 그대로 유지, 위 5개 append.

---

## 9. 코드화 대상 파일 후보 (Phase 2 · task-2627 layer 동반 · read-only classifier)

- **신규 classifier**: `anu_v3/callback_lifecycle_classifier.py` (또는 dispatch/) — evidence 입력 → (축A,축B,축C) 산출. **read-only · 순수 함수 · stateful writer 아님**.
- **artifact writer**: collector 경로(`dispatch/normal_fallback_callback_helper.py` launch_callback / fallback collector)에서 `<task>.callback_lifecycle.json` append (idempotent).
- **result.json 확장**: executor completion contract 가 fields 10~14 기록.
- **schema 상수**: enum 정의 단일 모듈(축A/B/C) — dispatch 와 anu_v3 양쪽 import.
- **회귀 테스트**: `tests/regression/test_callback_lifecycle_classifier.py` — 3 사례 fixture(2625/2628/2628+1) → 기대 (축A,축B,축C) assert + evidence-derivation 결정성.
- ★ daemon / long watcher / dual-purpose writer **금지**(no-dual-purpose doctrine). classifier 는 호출 시점 1회 판정.
- ★ task-2627 callback enforcement 가 origin/main 미반영인 한, 본 classifier 도 같은 미머지 계층 → **task-2627 reflection PR 과 동반 머지**(별도 un-merged 분산 금지).

---

## 10. Chair decision 필요 여부

1. 본 2축 스키마 초안 승인 여부 (Phase 1 단일소스 확정)
2. Phase 2 코드화 task 발행 여부 + task-2627 reflection PR 과 동반 머지 묶음 승인
3. 축C 태그 집합 확정(6개 + 확장 정책)
4. classifier 위치 = anu_v3/ vs dispatch/ (계층 결정 — anu_v3 미머지 부담 고려)
5. result.json fields 10~14 append 를 executor completion contract 에 강제할지

---

## 11. 금지 (회장 verbatim)

- callback 재발사 금지 · marker 삭제 금지 · foreign dirty 정리 금지 · push/PR/merge 금지 · production enforcement 완료 판정 금지 · **flat enum 단독 모델로 회귀 금지**
- (Phase 1) 코드 0 — 본 문서는 설계 단일소스. 코드화는 Phase 2 별도 위임.

끝
