From e1203166d974feb3341f3e2f2e5cc61539fdfbb8 Mon Sep 17 00:00:00 2001 From: Godopu Date: Mon, 7 Sep 2026 08:27:04 +0900 Subject: [PATCH] docs(reports): archive phase 2b unanimous pass review reports (a665d729, 7e7eea07, a7f1ee0e) --- .../report-a665d729.md | 69 +++++++++++++++++++ .../report-a7f1ee0e.md | 24 +++++++ .../report-7e7eea07.md | 50 ++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 .agents/reports/planner-reviewer-claude-01/report-a665d729.md create mode 100644 .agents/reports/planner-reviewer-codex-01/report-a7f1ee0e.md create mode 100644 .agents/reports/reviewer-creator-grok-01/report-7e7eea07.md diff --git a/.agents/reports/planner-reviewer-claude-01/report-a665d729.md b/.agents/reports/planner-reviewer-claude-01/report-a665d729.md new file mode 100644 index 0000000..c1da3ef --- /dev/null +++ b/.agents/reports/planner-reviewer-claude-01/report-a665d729.md @@ -0,0 +1,69 @@ +# Issue #5 Phase 2b — Round 4 교차 리뷰 (Codex Round 3 R4 후속 결함 수정) + +- **Job**: a665d729 / Reviewer: planner-reviewer-claude-01 +- **리뷰 대상**: 커밋 `8d7e010` "fix(issue-5): record an accepted intent, not a refused one" (기준 `9a651cc`) +- **범위 고정**: brief에 내장된 diff와 `git diff 9a651cc..HEAD -- . ':(exclude).agents/reports'`를 바이트 비교 → **동일**(15,916 B). 변경 파일 4개: `lib_py/delegation/job_subscriber.py`, `docs/contracts/issue-5/phase2b-autocomplete.md`, `docs/contracts/issue-5/task.md`, `tests/test_issue5_phase2b_autocomplete.py`. +- **동결 스냅샷**: `mam-loop-freeze.03Uqb9`의 `job_subscriber.py` md5 = `9a651cc` 시점과 동일(수정 **전**). engine/notifier/autocomplete/lib.sh는 HEAD와 동일. 즉 이 루프 자신의 구독자 데몬(pid 10863)은 수정 전 코드를 실행 중이며, 수정은 다음 루프 기동(새 동결)부터 효력을 갖는다. B-13 설계상 당연한 결과이고 diff의 결함이 아니다. + +## 1. 지적 사항 대조 (Codex Round 3) + +지적: `_Watcher.on_message`에서 `seen_intents[jid].add(identity)`가 단조 seq 가드보다 **앞서** 실행되어, 늦게 도착한 과거 패킷이 자기 intent를 "본 것"으로 태우고 뒤따르는 정당한 재시도가 드롭된다. + +수정 후 순서(`job_subscriber.py:144-179`, 직접 확인): +1. `data`가 dict가 아니면 intent 없음으로 취급(이전 코드의 `(data or {}).get` 경로 제거) +2. intent가 있으나 dict가 아니면 **드롭+경고** +3. identity 튜플 생성 → `in seen_intents` 대조를 `try/except TypeError`로 감싸 해시 불가면 **드롭+경고** +4. 이미 본 identity면 드롭(seq 바닥을 올리지 않음) +5. seq 단조 가드 +6. **가드 통과 후에만** `seen_intents[jid].add(identity)` +7. 감사 로그 `received` 기록 → 큐 투입 + +brief의 요구 2)·3) 모두 충족. 요구 3)의 시나리오(`last_seq=10`, seq=9 거부 → 동일 intent seq=11 수신 → `qsize()==1`)가 `test_r4b_a_stale_packet_does_not_burn_the_intent_for_its_own_retry`에 그대로 있다. + +## 2. 실측 재현 + +### 2-1. 신규 테스트 6종 — HEAD vs 수정 전 트리 +`9a651cc` 워크트리에 HEAD의 테스트 파일을 복사해 실행: + +| 결과 | 테스트 | +|---|---| +| 수정 전 **FAIL** | `stale_packet_does_not_burn_the_intent` (`seen_intents == {(7,'completed','T')}` 잔존), `unhashable_intent_is_refused` (`TypeError: unhashable type: 'list'` 콜백 탈출), `non_object_intent_is_refused` | +| 수정 전 PASS(보존 가드) | `duplicate_intent_does_not_advance_the_sequence_floor`, `absent_or_null_intent`, `real_retry_is_deduplicated_by_the_real_consumer` | + +task.md P2B-07의 "3종 역검증 실패 / 3종 보존" 주장과 **정확히 일치**. HEAD에서는 파일 전체 65 passed. + +### 2-2. 테스트 밖 엣지 — 실제 콜백에 직접 투입 (HEAD / 수정 전) + +| 입력 | 수정 전 | HEAD | +|---|---|---| +| `data`가 비어있지 않은 list / str | **`AttributeError` 콜백 탈출** | 정상 수신(intent 없음 취급) | +| intent의 `event` 필드가 dict(중첩 해시 불가) | **`TypeError` 콜백 탈출** | 드롭+경고, `last_seq` 불변 | +| 같은 wire seq QoS1 재전달(+intent) | 1건 | 1건 | +| 거부된 replay(seq 9) 뒤 바닥(1)보다 크고 9보다 작은 정상 이벤트(seq 4) | 수신 | 수신 | +| 필드가 전부 없는 intent `{}` 두 번 | 1건 수신, `(None,None,None)` 기록 | 동일 (O-1 참조) | + +`data`가 비객체인 경우는 task.md가 주장하지 않은 **추가 탈출 경로**인데 이번 수정으로 함께 닫혔다(paho 네트워크 스레드에서 콜백 예외는 해당 구독자의 모든 job 수신을 끊으므로 실제 가치가 있다). + +### 2-3. 종단 테스트의 전제 확인 +`test_r4b_a_real_retry_...`는 도달 불가 브로커(127.0.0.1:65432)로 `notifier.flush` 2회 후 감사 로그의 `published` 항목 2건을 실제 콜백에 투입한다. `publish_event.record_outcome`이 발행 성공 여부와 무관하게 `published` 감사 항목을 기록(G-1/G-2/G-4)함을 코드로 확인했으므로 전제가 성립하며, 두 항목의 wire seq가 다르고 `notification_intent`가 같음 → 1건만 큐에 남는다. + +### 2-4. 린트·정합성 +- `py_compile` OK. pyflakes/ruff는 venv에 없고 저장소에 line-length 설정도 없음(pyproject/setup.cfg/ruff.toml 부재). 신규 줄 중 `job_subscriber.py:161`(102자)이 100자를 넘지만 기존 줄(98·170·227·228)도 이미 넘으므로 관례 위반 아님. +- 테스트 실행 후 `git status` clean(샌드박스에 격리됨). HEAD 수집 총계 732 = 726 + 6. + +## 3. 판단 + +**결함 없음(blocking).** 지적된 순서 역전은 "대조는 가드 앞, 기록은 가드 뒤"로 정확히 고쳐졌고, 그 논리(중복은 시퀀스를 소비할 자격이 없고, 전달 안 된 이벤트는 본 것이 아니다)가 코드 주석·계약 문서·테스트에 일관되게 적혀 있다. 방어적 파싱 강화는 콜백 예외 탈출 경로 3가지(비객체 intent, 해시 불가 필드, 비객체 `data`)를 닫았다. + +## 4. 비차단 관찰 (다음 라운드/Phase 3 참고) + +- **O-1. 식별 필드가 전부 없는 intent는 "안정된 신원"이 아니다.** `{}`나 `{"foo":1}`은 identity `(None,None,None)`을 얻어 **첫 번째만 수신**되고 이후 같은 꼴은 모두 replay로 드롭된다. 정상 발행자(`notifier.intent_for`)는 세 필드를 항상 채우므로 실제 경로에서는 발생하지 않고, 발생하더라도 방향이 fail-closed(드롭)라 이중 적용은 없다. 다만 phase2b-autocomplete.md의 새 문장 "안정된 신원을 만들 수 없는 intent는 거부한다"와 코드가 정확히 맞으려면 `identity` 생성 직후 `if any(f is None for f in identity): 드롭+경고` 한 줄이면 된다. **검증**: HEAD 워크트리에 이 4줄을 적용하고 phase2b·phase1 테스트 131 passed, `{}`/`{"foo":1}`는 드롭되고 정상 intent는 1회 수신됨을 확인. +- **O-2. task.md P2B-07 문구의 개수.** "신규 회귀 6종(3종 실패 역검증, 3종 보존) + 종단 테스트"로 읽히지만 종단 테스트는 6종에 **포함**된다(`test_r4b_` 함수는 정확히 6개, 보존 3종 중 하나가 종단 테스트). "6종(그중 1종은 종단)"으로 고치면 된다. +- **O-3. 운영 노트.** 동결 스냅샷 원리상 현재 실행 중인 루프의 구독자는 수정 전 코드다. 이 라운드의 알림 재시도가 stale-first 상황을 만나면 옛 결함이 재현될 수 있으나, 재시작 후 디스크 폴백 + 멱등 ACK가 종단을 보증한다(기존 계약). 조치 불필요, 인지만. +- 이전 라운드 인계 사항(O-7 direct 잡의 `claim` 토큰 발급, `_delegate_py_bin` 앵커링, redispatch 시 shadow dispatch 갱신, `preexec_fn`)은 이번 diff 범위 밖이며 그대로 유효하다. + +## 5. 전체 스위트 + +`.venv/bin/python -m pytest tests -q -p no:cacheprovider` (백그라운드 1회, 정상 종료): **732 passed in 802.02s (0:13:22)**, FAILED/ERROR 0건. 이전 라운드 726 → 732 (+6, 신규 `test_r4b_*` 6종). 이번에는 이전 라운드(19ac4c53)에서 있었던 비정상 종료가 재현되지 않았다. + +[VERDICT: PASS] diff --git a/.agents/reports/planner-reviewer-codex-01/report-a7f1ee0e.md b/.agents/reports/planner-reviewer-codex-01/report-a7f1ee0e.md new file mode 100644 index 0000000..9e319e5 --- /dev/null +++ b/.agents/reports/planner-reviewer-codex-01/report-a7f1ee0e.md @@ -0,0 +1,24 @@ +# Issue #5 Phase 2b final correction review + +Job: a7f1ee0e +Reviewer: planner-reviewer-codex-01 (planner,reviewer) +Reviewed HEAD: 8d7e010; correction relative to 9a651cc. + +The remaining R4 ordering defect is resolved. No blocking findings in the requested correction; no supplementary edits or escalation required. + +## Assessment + +- `_Watcher.on_message` now adds an intent to seen_intents only after the wire sequence passes the monotonic check. A rejected stale packet cannot suppress a subsequent valid retry. +- Existing accepted-intent duplicate rejection remains intact. +- Non-object intent values and unhashable identity components are rejected without escaping the callback. Access to data is guarded by an object-type check. +- The change is confined to subscriber validation, tests and documentation. Previously reviewed dispatch-token/CAS, manifest verification and notification settlement fixes remain unchanged. + +## Verification + +- `./.venv/bin/python -m pytest tests/test_issue5_phase2b_autocomplete.py -q`: **65 passed in 1.28s**. +- Independent real-subscriber probe: initialize last_seq=10; send the same intent with wire sequences 9, 11, 12. Result: **one queued event, last_seq=11**. Thus stale-first rejection, fresh-retry acceptance and subsequent duplicate rejection all hold together. +- `git diff --check 9a651cc..HEAD`: clean. +- Working tree clean before writing the assigned report; no production or fixture files modified by this reviewer. +- Full preexisting suite was not rerun in this review. This verdict addresses the requested correction and carries forward the prior resolved findings; it does not claim independent full-suite or other-reviewer approval. + +[VERDICT: PASS] diff --git a/.agents/reports/reviewer-creator-grok-01/report-7e7eea07.md b/.agents/reports/reviewer-creator-grok-01/report-7e7eea07.md new file mode 100644 index 0000000..cc50074 --- /dev/null +++ b/.agents/reports/reviewer-creator-grok-01/report-7e7eea07.md @@ -0,0 +1,50 @@ +# Issue #5 Phase 2b Round 3 review (`8d7e010`) + +- **Reviewer**: `reviewer-creator-grok-01` +- **Job**: `7e7eea07` +- **HEAD**: `8d7e010` (`fix(issue-5): record an accepted intent, not a refused one`) +- **Prior Grok residual**: `0d502d33` item 1 — `seen_intents` recorded before the seq check +- **Tests this turn**: `pytest tests/test_issue5_phase2b_autocomplete.py tests/test_issue5_phase2a_loop.py` → **156 passed**. Phase 2b file: **65 collected**. Collect-only **732**. `git diff --check 9a651cc..HEAD` clean. + +The live callback was re-read and the stale-first probe from `0d502d33` was re-run against this tree. + +--- + +## Adjudication + +[ADJUDICATION: SUSTAINED] + +`job_subscriber.py:143-179` now splits the two operations: + +1. **Lookup** of `identity in seen_intents` still runs *before* the seq guard, so a replay with a fresh wire seq is dropped. +2. **`seen_intents.add`** runs *after* `seq > last_seq` and after `last_seq` is advanced. A refused packet does not mark its intent as accepted. + +Malformed `notification_intent` (non-object, or unhashable field) is dropped inside defensive parsing with a warning; `TypeError` does not escape the MQTT callback. + +Required regression `test_r4b_a_stale_packet_does_not_burn_the_intent_for_its_own_retry`: `last_seq=10`, seq=9 refused, same intent at seq=11 accepted, `qsize()==1`, a further replay stays at 1. + +**Reproduced this turn** (same probe that failed on `9a651cc`): + +``` +stale seq=9 last=10 -> qsize=0 seen=set() last=10 +retry seq=11 same intent -> qsize=1 +replay seq=12 -> qsize=1 +unhashable: raised=False qsize=0 last=0 +``` + +Also covered: refused replay does not raise `last_seq` (`test_r4b_a_duplicate_intent_does_not_advance_the_sequence_floor`); end-to-end two `notifier.flush` attempts with distinct wire seqs land once on the real callback (`test_r4b_a_real_retry_is_deduplicated_by_the_real_consumer`). + +--- + +## Residuals (unchanged, not this patch) + +1. No production caller of `evaluate` / `recover` (Phase 3; §2-1). +2. `started` does not mint `dispatch_token`. +3. Bookkeeping may write any `pending_notification` value, not only `None`. +4. `pick_pending` split-lock. + +No `[ESCALATE: PLANNER]`. This is the local order fix the residual asked for. + +--- + +[VERDICT: PASS]