# task-2775+9 완료 보고서 — `_reverse_line_iter` blank-line correctness micro-fix

> 작성: 오딘(dev2 팀장) | 일시: 2026-06-27 KST | 팀: dev2-team | PR #255 (`task/task-2775-dev2`)
> 성격: efficiency 아님 · correctness cleanup 1회 · **ACTIVE=false / merge 금지 / real fire 0**

---

## SCQA 요약

**S (Situation)**: task-2775 PR #255는 limited activation 3경계(kill/N/T) bound gate 선행 안전 결선으로,
+8에서 `_count_active_window_pickups`가 ledger를 newest-first chunk reverse(`_reverse_line_iter`)로 스캔하도록
구조가 완성되어 2775 회귀 55건 green 상태였다.

**C (Complication)**: fresh-head Gemini 리뷰가 fresh MEDIUM 1건(driver:536)을 지적 — `_reverse_line_iter`가
**파일이 빈 줄로 시작**(`b"\nfoo\n"` 등)할 때 루프 종료 후 `buf == parts[0] == b""`가 되어 `if buf:`가 False →
**첫 빈 라인을 누락**. `__all__`로 노출된 범용 유틸이라 correctness edge로 남으면 MERGE_CANDIDATE 미확정.

**Q (Question)**: reverse scan 구조(chunk seek SEEK_END)를 유지하면서, 빈 줄로 시작하는 파일의 첫 빈 라인을
누락 없이 yield하되 빈 파일은 0건 yield하도록 정확히 구분할 수 있는가?

**A (Answer)**: 함수 시작에 `if pos == 0: return`(빈 파일 → yield 0건) 분기를 추가하고, 루프 종료 후
`if buf:` 조건을 제거해 **무조건 final buf yield**(루프가 1회 이상 돌았다면 final buf는 파일의 첫 라인이므로
빈 줄도 항상 반환). 9줄(+7/-2) 변경으로 driver:536 close. 2775 63 passed(신규 8건 포함), 2760 36 passed,
2721 driver 34 passed, L1 런타임 스모크 3/3 PASS. efficiency/구조 재작업 0.

---

## driver:536 대응 내용 (Gemini fresh MEDIUM)

수정 파일: `dispatch/anu_pickup_driver.py` · `_reverse_line_iter` (508~541줄)

수정 전(버그):
```python
    fh.seek(0, os.SEEK_END)
    pos = fh.tell()
    buf = b""
    while pos > 0:
        ...
    if buf:                                # 빈 줄 시작 파일에서 False → 첫 빈 라인 누락
        yield buf.decode("utf-8", "ignore")
```

수정 후:
```python
    fh.seek(0, os.SEEK_END)
    pos = fh.tell()
    if pos == 0:
        return  # 빈 파일 → yield 0건 (첫 조각 오인 방지)
    buf = b""
    while pos > 0:
        ...
    # ★ task-2775+9: 루프가 1회 이상 실행됐다면 buf 는 파일의 첫 라인이다.
    #   빈 줄로 시작하는 파일도 그 첫 빈 라인을 누락하지 않도록 항상 yield.
    yield buf.decode("utf-8", "ignore")
```

핵심 semantics: 비어있지 않은 파일은 `reversed(data.split(b"\n"))`와 완전 일치, 빈 파일만 예외로 0건.

## 범위 준수 증거

- 변경 파일 **정확히 2개** (expected_files 일치): `git diff --stat ac0db12d..0d5bff6b`
  - `dispatch/anu_pickup_driver.py` (+7/-2)
  - `tests/regression/test_limited_activation_bounds_2775.py` (+115)
- forbidden_paths(preflight/test-2760/governor/runner/launcher/`.github/**`/finish-task.sh/flag 파일) **무수정**.
- `_count_active_window_pickups` stop 조건(`count >= max_count` 단일) · `processed_at < activation_epoch` break
  금지 · ledger/max_count/epoch 정책 **무회귀**.
- reverse scan 구조(chunk seek SEEK_END) **유지** — 전체 read 후 reversed/readlines/list(file) 금지 준수.

## reverse iterator blank-line 테스트 결과 (헤임달, 신규 8건)

1. `test_reverse_iter_leading_blank_line_not_dropped` — `"\nfoo\n"` → `["","foo",""]` (핵심 회귀)
2. `test_reverse_iter_leading_consecutive_blank_lines` — `"\n\nb\n"` → `["","b","",""]`
3. `test_reverse_iter_empty_file_yields_nothing` — 0바이트 → `[]`
4. `test_reverse_iter_single_newline_file` — `"\n"` → `["",""]`
5. `test_reverse_iter_trailing_newline_no_regression` — `"A\nB\n"` → `["","B","A"]` (trailing 무회귀)
6. `test_reverse_iter_blank_line_at_chunk_boundary` — `"aaaa\n\nbbbb\n"` chunk_size=4 → `["","bbbb","","aaaa"]`
7. `test_reverse_iter_matches_forward_split_general` — 4입력 × chunk_size 1/3/8192, `reversed(split)` 일치
8. `test_count_active_window_with_leading_blank_ledger_lines` — ledger 앞 빈 줄 2개 + after-epoch 2건 → count 2

신규 선택 실행: 11 passed. 전체 2775: **63 passed (0.28s)**.

## +8 bounded reverse 성능 무회귀 증거

2775 파일 내 +8 성능 회귀 테스트가 전부 green:
- `test_count_reverse_skips_historical_front_when_max_reached` — historical 1000건 + active 3건, max_count=3
  시 parse 호출 **3회**(전수 스캔 0).
- `test_count_reverse_bounded_bytes_not_whole_file_read` — historical 2000건 + active 3건, 읽은 바이트
  `< total_size` 및 `<= 4096*3`(tail 일부만).
- `test_count_reverse_under_max_reads_to_start_exact` / `test_count_reverse_mixed_epoch_no_regression` /
  `test_count_reverse_excludes_missing_and_unparseable` — under-count 0, epoch-scope/제외 정책 무회귀.

빈 줄 처리 변경은 final-buf yield 1건만 추가하므로 chunk 경계/early-break 동작에 영향 없음(O(필요분) 유지).

## L1 스모크테스트 결과

- 서버 재시작: **해당없음** (순수 함수/유틸 변경, 서버 컴포넌트 아님)
- API 응답 확인: **해당없음** (HTTP 엔드포인트 없음)
- 실런타임 구동(subprocess/정제 카테고리): **3/3 PASS**
  - L1-1 leading-blank `b"\nfoo\n"` → `["","foo",""]` PASS (수정 대상 버그 실재현·해소 확인)
  - L1-2 empty-file → `[]` PASS
  - L1-3 실제 ledger(앞 빈 줄 2개 + after-epoch 3건) `_count_active_window_pickups` → 3 PASS
- 스크린샷: 해당없음 (백엔드 유틸, UI 없음)

## 테스트 결과 종합

- `python3 -m py_compile` (driver + test) — OK
- 2775 전체 — **63 passed**
- test-2760 (`test_p0b_event_strategy_2760.py` + wiring) — **36 passed**
- 2721 driver 인접 (`test_anu_pickup_driver_2721.py`) — **34 passed**
- L1 런타임 스모크 — 3/3 PASS
- (참고) `test_base_source_isolation_2729p9.py`는 wt-2729p9 전용 환경 격리 테스트로 collection 단계에서
  경로 하드코딩 assertion 실패 — **본 변경과 무관**, 대상 파일 직접 지정으로 우회 검증.

## ACTIVE=false / real fire 0 증거

- 코드 변경은 `_reverse_line_iter`(읽기 전용 iterator) + 테스트뿐. flag write/systemctl/activation flag 생성/
  canary/recurring/backlog consumption **0건**.
- 신규 테스트는 tmp_path 격리 — 프로덕션 ledger/flag 미접촉.

## fresh-head Gemini 재리뷰 필요 여부

**필요** — head가 `ac0db12d` → `0d5bff6b`로 갱신(+9 commit 2건). +9 이후 경로(task 정의)대로
fresh-head Gemini 재리뷰에서 High 0 / Critical 0 / fresh Medium 0 / efficiency 0 + CI green 확인 후
**ANU 독립검증 → MERGE_CANDIDATE**로만 보고. **본 task에서 merge/thread resolve 금지.**

## 머지 판단

- 머지 필요: **No (본 task에서 금지)** — manual merge policy, ANU 독립검증·Gemini 재리뷰·CI 후 MERGE_CANDIDATE.
- 브랜치: `task/task-2775-dev2`
- 워크트리: `/home/jay/workspace/.worktrees/task-2775-dev2`
- 머지 의견: 9+115줄 최소 correctness diff, expected_files 2개 한정, forbidden 무수정, 회귀 0. 안전하나
  승인 영역(merge/thread resolve)은 OWNER/회장 권한이므로 ANU 핸드오프.

## SHA 정보

- PR #255 base SHA(merge-base origin/main): `e8925d91959fee06d003e14c93d15facce28810c`
- 이전 head: `ac0db12d50a4f4f0ce21a29b246406b77a8b9883`
- **새 head: `0d5bff6b47a44c922945014fdf9c2b33a2a73e76`**
- diff 파일 수: **2** (driver +7/-2, test +115)

## 모델 사용 기록

- 토르(백엔드, code fix): **sonnet** — 로직 수정
- 헤임달(테스터, 회귀 8건): **sonnet** — 테스트 설계/작성
- 통합 검증·L1·보고·마감: 오딘(팀장, Opus) — 판단/검토
- haiku 미사용 (correctness 로직·테스트 설계는 sonnet 이상 규칙 준수).

## callback_schedule_created

`false` (executor는 callback cron 직접 등록 안 함 — OS-level pickup runner가 owner-proof 후 결정론 closeout 소유).
relay_hints 부재 → deterministic closeout. ANU normal collector(owner_key `c119085addb0f8b7`) 별도 spawn.
