# callback collector canonical-root resolver spec (Core hardening · read-only · merge 실행 0) — 260523

회장 결정(2026-05-23 task-2635+1 ACCEPT 직후): task-2635+1 처리 중 ANU collector 가 autoset/fresh cwd 기준 result/report 미발견 false-negative 발생(CALLBACK_COLLECTOR_CONTEXT_MISMATCH). real merge executor wiring 전 collector context-mismatch 차단 hardening 필수.

기반: `feedback_callback_session_discontinuity_and_canonical_root_260518.md` + `feedback_normal_callback_not_registered_variant_260523.md` + `system_normal_callback_registration_implementation_spec_260523.md` + PR #138 1e24d79e 머지분.

---

## 1. 사고 분류 (재확인)

| 항목 | 값 |
|---|---|
| 사고 분류 | **CALLBACK_COLLECTOR_CONTEXT_MISMATCH** |
| Critical7 해당 | ❌ 아님 |
| 봇 산출물 결함 | ❌ 아님 (canonical /home/jay/workspace 에 정상 존재) |
| 영향 | ANU collector cwd /home/jay/.cokacdir/workspace/autoset 기준 탐색 → "참조 파일 없음" false-negative → 회장 수동 텔레그램 relay 의존 → 거버넌스 자동화 break |

---

## 2. 목표 (회장 10 항목 verbatim)

1. callback envelope `canonical_root` 필드 우선 사용
2. `canonical_root` 부재 시 default `/home/jay/workspace`
3. `result_path` / `report_path` relative → `canonical_root` 기준 resolve
4. **현재 cwd 기준 탐색 primary 사용 금지**
5. cwd ≠ canonical_root 시 **CALLBACK_COLLECTOR_CONTEXT_MISMATCH** 기록
6. result/report 미발견 시 lookup 순서: canonical_root → workspace fallback → 명확한 missing classification
7. autoset/fresh cwd 에서 canonical /home/jay/workspace 산출물 찾는 fixture
8. canonical_root 누락 / 잘못된 canonical_root / relative path / absolute path 케이스 regression
9. callback envelope 는 단순 로그가 아니라 **ANU collector action trigger** 임을 명확히 검증
10. sendfile-only 와 normal callback envelope 구분 유지 (collector trigger 시점에 sendfile envelope 무시)

---

## 3. canonical_root resolve 규칙 (정본)

```
CANONICAL_ROOT_RESOLUTION_ORDER:
  1. envelope.canonical_root (explicit override)         — priority 1
  2. CANONICAL_ROOT_DEFAULT = "/home/jay/workspace"       — fallback
  3. ★ cwd-based lookup 사용 금지 (primary path 에서 0)
```

```
PATH_RESOLUTION_RULE:
  absolute_path → 그대로 사용 (canonical_root 무시)
  relative_path → os.path.join(canonical_root_resolved, relative_path) 로 확정
  empty_or_none → MISSING classification
```

```
COLLECTOR_LOOKUP_ORDER:
  1. canonical_root + result_path     (primary)
  2. canonical_root + report_path     (secondary)
  3. (fallback) /home/jay/workspace + path (canonical_root 명시 무시)
  4. (missing) MISSING_BOTH_PATHS classification
```

```
CONTEXT_MISMATCH_DETECTION:
  if os.getcwd() != canonical_root_resolved:
      log CALLBACK_COLLECTOR_CONTEXT_MISMATCH event
      proceed with canonical_root_resolved (do not use cwd)
```

---

## 4. envelope schema 확장

기존 `utils/callback_envelope_schema.py` 의 REQUIRED_ENVELOPE_KEYS 에 추가 또는 별도 optional 필드:
- `canonical_root` (optional · str · default `/home/jay/workspace`) — workspace 절대 경로
- validate_envelope 에 canonical_root absolute path 검증 추가
- 기존 5축 schema 와 직교 (5축 변경 0)

---

## 5. 신규 helper 모듈

### 5.1 `utils/canonical_root_resolver.py` — 신규
- `resolve_canonical_root(envelope, default="/home/jay/workspace") -> str`
  - envelope.canonical_root 우선, 없으면 default
  - absolute path 단언
- `resolve_path(envelope, path_field, canonical_root=None) -> str`
  - path_field = `result_path` / `report_path`
  - absolute path 면 그대로
  - relative → canonical_root resolve
- `detect_context_mismatch(envelope, current_cwd=None) -> {mismatch: bool, cwd, canonical_root, recorded_event_id?}`
  - cwd 비교 + 차이 시 mismatch=true
  - mismatch event JSONL 기록 (`memory/events/callback_collector_context_mismatch.jsonl`)
- `find_artifact(envelope, path_field) -> {found, resolved_path, lookup_attempts, classification}`
  - LOOKUP_ORDER 적용 + MISSING classification

### 5.2 `utils/anu_collector_action_trigger.py` — 신규 (또는 dispatch/collector_hooks.py)
- `is_callback_action_trigger(envelope) -> bool`
  - normal callback (`registration_result_status=REGISTERED` + `delivery_method=anu_cron_callback`) → True
  - sendfile-only (`delivery_method=sendfile_only` 또는 `SENDFILE_ONLY` status) → **False** (단순 로그)
- `enqueue_collector_action(envelope) -> action_record`
  - trigger 여부 단언 후 action queue 진입
- regression 으로 sendfile envelope 가 trigger 로 잘못 분류되지 않음 단언

---

## 6. CALLBACK_COLLECTOR_CONTEXT_MISMATCH 기록 방식

- 위치: `memory/events/callback_collector_context_mismatch.jsonl` (append-only JSONL)
- schema:
  ```
  {
    "schema": "callback_collector_context_mismatch.v1",
    "ts_kst": "...",
    "task_id": "...",
    "cwd": "<os.getcwd() value>",
    "canonical_root_envelope": "<envelope.canonical_root or null>",
    "canonical_root_resolved": "<final>",
    "delta_paths": ["..."],
    "decision": "PROCEED_WITH_CANONICAL_ROOT"   # always canonical, never cwd
  }
  ```
- Critical7 아님 (운영 hygiene · 거버넌스 메트릭)
- 누적되면 dashboard/alert 후보 (별도 task)

---

## 7. 필수 fixture (frozen, live 의존 0)

`tests/fixtures/callback_collector_canonical_root/<scenario>/{evidence.json, expected.json, PROVENANCE.md}` (4 시나리오 최소 권장 6):

| 시나리오 | input | expected |
|---|---|---|
| `canonical_root_explicit` | envelope.canonical_root=/home/jay/workspace + relative result_path | resolve to canonical_root + result_path |
| `canonical_root_missing_default` | envelope.canonical_root=None | use default /home/jay/workspace |
| `canonical_root_wrong_absolute` | envelope.canonical_root=/wrong/path | mismatch detection · still resolve to /wrong/path (envelope honors explicit) + log mismatch |
| `relative_paths_resolve_correctly` | result_path/report_path relative | resolved absolute paths |
| `absolute_paths_passthrough` | result_path/report_path absolute | passthrough (no canonical_root concat) |
| `cwd_in_autoset_canonical_in_workspace` | cwd=/home/jay/.cokacdir/workspace/autoset · canonical_root=/home/jay/workspace | MISMATCH event 기록 + canonical_root 사용 |
| `sendfile_only_not_trigger` | delivery_method=sendfile_only · result_status=SENDFILE_ONLY | is_callback_action_trigger=False |
| `normal_callback_is_trigger` | delivery_method=anu_cron_callback · result_status=REGISTERED + DELIVERED | is_callback_action_trigger=True |

총 8 시나리오 × 3 files = **24 fixture files**.

---

## 8. 필수 regression

`tests/regression/test_canonical_root_resolver.py`:
- `resolve_canonical_root` 케이스 (envelope override · default · invalid relative root rejection)
- `resolve_path` 케이스 (absolute passthrough · relative concat · empty MISSING)
- 8 fixture parametrized 단언

`tests/regression/test_collector_context_mismatch.py`:
- cwd != canonical_root 시 mismatch event 기록 단언
- mismatch event JSONL append schema validator
- cwd == canonical_root 시 event 미기록 단언

`tests/regression/test_collector_action_trigger.py`:
- `is_callback_action_trigger` normal callback → True
- sendfile-only → False
- registration_result_status=NOT_REGISTERED → False (fail-closed 일관)
- sendfile envelope 가 collector action queue 에 push 되지 않음 단언

`tests/regression/test_autoset_cwd_canonical_lookup.py`:
- 작업 디렉터리를 autoset (또는 /tmp 등 임의) 으로 chdir 한 상태에서 canonical root 기준 산출물 발견 단언
- subprocess 실호출 0 (monkeypatch os.getcwd)

---

## 9. expected_files (task-2636 범위)

신규:
- `utils/canonical_root_resolver.py`
- `utils/anu_collector_action_trigger.py` (또는 `dispatch/collector_hooks.py`)
- `tests/fixtures/callback_collector_canonical_root/<8 시나리오>/{evidence.json,expected.json,PROVENANCE.md}` (24 files)
- (선택) `tests/fixtures/callback_collector_canonical_root/INDEX.md`
- `tests/regression/test_canonical_root_resolver.py`
- `tests/regression/test_collector_context_mismatch.py`
- `tests/regression/test_collector_action_trigger.py`
- `tests/regression/test_autoset_cwd_canonical_lookup.py`

수정 (최소 결선):
- `utils/callback_envelope_schema.py` — `canonical_root` optional 필드 추가 + validator
- `dispatch/finalize_hooks.py` — envelope build 시 `canonical_root=CANONICAL_ROOT_DEFAULT` 채움 (옵션)

총 ~32 files. **프로덕션 영향**:
- schema 1 필드 추가 (optional · backward-compat 100%)
- finalize_hooks 1 줄 추가 (envelope canonical_root 기본값)
- replacement_pr_runner / finish-task.sh / merge_ready_classifier / merge_ready_dryrun_executor **무수정**

---

## 10. 안전 불변식

- ANU key `c119085addb0f8b7` 하드코딩 단일 출처 유지 (변경 0)
- envelope UTF-8 ≤3900 bytes hard limit 유지
- live cokacdir CLI 실호출 0 (regression monkeypatch / dry-run)
- subprocess 실호출 0
- merge/push/PR/cron/branch-protection/admin-override 호출 0
- replacement_pr_runner / finish-task.sh / merge_ready_classifier / merge_ready_dryrun_executor **무수정**
- expected_files 외부 수정 0
- 5축 schema 변경 0 (canonical_root 는 6번째 직교 필드)
- cwd-based primary lookup 코드 경로 0 (정적 가드)

---

## 11. 자동수렴 원칙

- Gemini medium/style + expected_files 내부 + Critical7 0 + credential expansion 0 → 자동수렴
- expected_files 내부 non-critical HIGH 도 자동수렴
- 회장 보고 트리거: Critical7 / credential expansion / expected_files 밖 수정 / admin override / replacement_pr fail / post-merge smoke fail

---

## 12. 금지 (회장 verbatim)

- real auto-merge executor 구현 금지
- auto-merge 활성화 금지
- NL intake 코드 구현 금지
- foreign dirty 정리 금지
- replacement_pr_runner 수정 금지
- finish-task.sh 수정 금지
- production service task 와 혼합 금지
- PR #138 산출물과 섞기 금지 (별개 hardening)
- 5축 status schema 변경 금지 (canonical_root 는 별개)

---

## 13. finalize 프로토콜 (★ BOT App token 부재 — 로컬 한정)

1. base = 최신 origin/main 1e24d79e (PR #138 머지분) clean worktree
2. 신규 helper 2 + fixture 24 + regression 4 + schema/hook 최소 수정 전부 PASS + full new-fail 0
3. **로컬 commit만** (push/PR/merge 금지)
4. ANU normal completion callback — **신규 canonical_root resolver 검증 자기 사용**: envelope 에 `canonical_root=/home/jay/workspace` 명시 + collector 가 본 envelope 의 canonical_root 정확히 resolve 함을 자기검증
5. envelope 5축 정합 유지 (REGISTERED + DELIVERED + UNCONFIRMED + schedule_id non-null 등)
6. callback envelope UTF-8 ≤3900 bytes
7. executor 시작/종료 ts·로컬 commit SHA 명기

이후 ANU: 봇 로컬 commit fresh main 재적층 → OWNER push → PR open → Gemini 자동수렴 → 회장 보고 → 회장 merge 승인.

---

## 14. frozen anchor (D-SPEC-EXACTNESS)

- ANCHOR-1: "envelope.canonical_root 우선 · default /home/jay/workspace · cwd-based primary lookup 코드 경로 0"
- ANCHOR-2: "cwd != canonical_root 시 CALLBACK_COLLECTOR_CONTEXT_MISMATCH 기록 + canonical_root 사용 (cwd 결정 권한 0)"
- ANCHOR-3: "8 시나리오 fixture (canonical_root override/default/wrong absolute/relative resolve/absolute passthrough/autoset cwd mismatch/sendfile not trigger/normal callback trigger)"
- ANCHOR-4: "5축 status schema 직교 유지 — canonical_root 는 6번째 별개 필드 · 5축 변경 0"
- ANCHOR-5: "sendfile-only 와 normal callback envelope 구분 (is_callback_action_trigger 단언)"
- ANCHOR-6: "replacement_pr_runner / finish-task.sh / merge_ready_classifier / merge_ready_dryrun_executor 무수정 · real auto-merge executor 미승인 유지"
