# task-2775+8 보고서 — PR #255 ledger newest-first reverse chunk scan micro-fix (ACTIVE=false)

상태: `TASK2775_PLUS8_REVERSE_LEDGER_SCAN_MICROFIX_DONE_MERGE_CANDIDATE`
담당: dev2-team (오딘 팀장 / 토르 백엔드 / 헤임달 테스터)
일시: 2026-06-27 KST · ACTIVE=false · real fire 0

## S (Situation)
+7에서 `_count_active_window_pickups`는 `count >= max_count` early-stop·under-count 0·117 passed는 확보됐으나, 스캔 방향이 여전히 **순방향(oldest-first, `for line in fh:`)**이었다. active window(최신 entry)는 ledger 파일 끝에 있어, max_count early-break가 있어도 active window 도달 전 historical entry를 전부 거치는 구조가 남았다(Gemini MEDIUM driver:538 — "O(N) Ledger Scan from the Beginning").

## C (Complication)
회장 승인은 **+8 micro-fix 1회**(=+7 미완성 reverse scan 보정)만 허용. 가짜 reverse(전체 read 후 reversed) 금지. 범위는 driver·test **2파일**, 새 PR 금지(PR #255 같은 브랜치 commit 추가). merge/activation/real fire 금지.

## Q (Question)
전체 ledger를 메모리에 올리지 않고, 파일 끝에서부터 필요한 line만 읽는 **진짜 bounded reverse iterator**를 2파일 안에서 안전하게 닫을 수 있는가?

## A (Answer)
가능. chunk 기반 reverse line iterator로 닫았다. **구조 재설계(`LEDGER_COUNT_STRUCTURE_REDESIGN_REQUIRED`) 불필요.**

---

## 수정 파일 (정확히 2개)
- `dispatch/anu_pickup_driver.py` (+51줄): `_reverse_line_iter` 신규 + `_count_active_window_pickups` 루프 교체 + `__all__` 등록 + docstring 보강
- `tests/regression/test_limited_activation_bounds_2775.py` (+175줄): reverse scan 회귀 8건 + 헬퍼/심볼 바인딩

base(main 분기점): `e8925d91` · 이전 head: `210ff3ce` · **새 head: `ac0db12d`** · diff 파일 수: **2**

## reverse scan 구현 방식
```python
def _reverse_line_iter(fh, *, chunk_size=8192):
    fh.seek(0, os.SEEK_END)
    pos = fh.tell()
    buf = b""
    while pos > 0:
        read_size = chunk_size if pos >= chunk_size else pos
        pos -= read_size
        fh.seek(pos); chunk = fh.read(read_size)
        buf = chunk + buf
        parts = buf.split(b"\n")
        buf = parts[0]               # 맨 앞 미완성 조각 보류(다음 과거 chunk와 합침)
        for part in reversed(parts[1:]):
            yield part.decode("utf-8", "ignore")
    if buf:
        yield buf.decode("utf-8", "ignore")
```
- `seek(SEEK_END)` 후 chunk_size 바이트씩 **뒤로** 읽으며, 완성된 line만 newest-first로 yield.
- consumer가 break하면 **더 과거 chunk를 읽지 않음** → active window(파일 끝)만 읽고 멈춤.
- `_count_active_window_pickups`는 `open(path, "rb")` + `_reverse_line_iter`로 스캔. stop 조건은 `count >= max_count` 하나뿐. `processed_at < activation_epoch`로 break 금지(단조성 가정 금지 → under-count 0).

## "전체 read 후 reversed가 아님" 입증 근거
1. **금지 패턴 0건**: `grep -nE "read_text|readlines|splitlines\(\)\)|list\(fh\)"` + reverse → NONE.
2. **bounded bytes 테스트**(`test_count_reverse_bounded_bytes_not_whole_file_read`): historical 2000건 + active 3건 파일에서 active 3줄만 소비 시, 읽은 바이트 `cf.bytes_read < total_size` AND `<= 4096*3` 단언 PASS. 전체 readlines면 total_size 전부 읽었어야 함.
3. **reversed() 적용 범위**: 단일 chunk 내부 line 목록(≤ chunk_size + 잔여)에만 적용 — 전체 파일을 뒤집지 않음.

## max_count 도달 후 과거 ledger 미스캔 (테스트 결과)
- `test_count_reverse_skips_historical_front_when_max_reached`: 파일 앞 historical **1000건** + 끝 active 3건, `max_count=3`. `_parse_processed_at_to_unix` 호출 횟수 **정확히 3회**(순방향이면 1003회) → historical front 미스캔 입증. PASS.
- **L1 스모크(실런타임)**: 실제 5003줄(529KB) ledger에서 `max_count=3` → count=3, parse 호출 **3회**(순방향이면 5003회). N미만(`max_count=10`) → count=3 정확(under-count 0).

## correctness 무회귀
- `test_count_reverse_under_max_reads_to_start_exact`: 앞 500건 + 끝 active 2건, max_count=3 → 정확히 2 (under-count 0).
- `test_count_reverse_mixed_epoch_no_regression`: epoch 전/후 interleave → after-epoch 3건 정확.
- `test_count_reverse_excludes_missing_and_unparseable`: processed_at 부재/파싱실패/naive 제외 → 정상 2건만.
- reverse iterator 단위: newest-first 순서 / chunk 경계 분할(chunk_size=4) / trailing newline 없음 전부 PASS.

## 테스트 결과
- **2775 bounds: 55 passed** (기존 47 + 신규 8) — +7 thread5 fail-closed(`test_t_elapsed_typeerror/valueerror_fail_closed`)·+7 thread4 0644(`test_atomic_disabled_write_mode_is_0644`) 무회귀 포함.
- **test-2760: 36 passed** (`test_p0b_event_strategy_2760.py` + `_wiring_2760.py`).
- **2721 driver 인접: 34 passed** (`test_anu_pickup_driver_2721.py`).
- `python3 -m py_compile` driver+test: OK.
- expected_files 밖 수정 **0** (`git diff --stat 210ff3ce..ac0db12d` = 2파일).

## L1 스모크테스트 결과 (필수 기록)
- 서버 재시작: 해당없음 (내부 ledger-scan 로직, 서버 미관여)
- API 응답 확인: 해당없음 (subprocess/순수 함수 — curl 대상 아님)
- 스크린샷: 해당없음 (UI 없음)
- **실런타임 스모크(서버 대체 검증)**: 5003줄 ledger 실파일 생성 → `_count_active_window_pickups(max_count=3)` 실행 → count=3, parse 3회만 호출(historical 5000건 미스캔), N미만 정확 → **PASS**

## ACTIVE=false / real fire 0 증거
- 코드 변경은 ledger **읽기(count)** 경로뿐. wake/cron/systemd/flag 생성 일절 없음.
- merge·activation·real fire·`systemctl`·activation flag 생성 **0**. PR #255 같은 브랜치 commit 추가만(새 PR 0).
- forbidden_paths(preflight/test-2760/governor/runner/owner-proof/.github/finish-task.sh/flag 파일) 수정 **0**.

## fresh-head Gemini 재리뷰 필요 여부
**필요.** 본 보고는 MERGE_CANDIDATE 제출일 뿐 — PASS여도 **머지 금지**. ANU 독립검증 → fresh-head(`ac0db12d`) Gemini 재리뷰 → CI 확인 후 머지 판단.
- ★ 메타 가드: +8 fresh-head Gemini 재리뷰에서 efficiency/O(N)/ledger scan 계열 finding이 HIGH/MEDIUM 재발 시 → +9 micro-fix 금지, 즉시 `LEDGER_COUNT_STRUCTURE_REDESIGN_REQUIRED_ACTIVE_FALSE` 보고(다음 방향=별도 상태화).

## 머지 판단
- 머지 필요: Yes (ANU/Gemini/CI 검증 후) — 팀장 직접 머지 금지(manual policy)
- 브랜치: `task/task-2775-dev2` (PR #255)
- 워크트리 경로: `/home/jay/workspace/.worktrees/task-2775-dev2`
- 머지 의견: 2파일 범위 준수, 가짜 reverse 0, 회귀 전부 PASS, 실런타임 스모크로 O(N) 해소 입증. High 위험 없음. ANU 독립검증·Gemini 재리뷰 통과 시 머지 권고.

## 모델 사용 기록
- 토르(백엔드, driver 구현): sonnet
- 헤임달(테스터, 회귀 8건): sonnet
- 오딘(팀장): 설계·검증·통합·보고 (Opus, 직접 코딩 없음)
- haiku 미사용.

## 발견 이슈 및 해결
- `pytest -k 2760`가 regression 디렉토리 전체 수집 중 무관한 `test_base_source_isolation_2729p9.py`의 사전존재 collection 에러에 걸림 → 2760 테스트 파일을 직접 경로 지정해 우회(36 passed). 본 작업 변경과 무관한 기존 이슈.
- pyright "Import dispatch could not be resolved" 경고는 테스트의 파일위치-직접로드 부트스트랩 패턴 때문에 발생하는 기존 경고(64행 등) — 런타임 import는 정상(55 passed).
