# ops-doc-reconcile-intake M1 — 구현 노트

> 계획서 `.claude/plans/ops-doc-reconcile-intake-m1.plan.md` (plan_hash `sha256:d33f14cc…`, 동결).
> 이 파일이 implement 단계의 리뷰 절 · 이탈 · 착수 실측을 싣는다. 계획서 본문에 절을 붙이면 hash-plan 이 바뀌어
> plan-codex receipt 가 stale 이 되기 때문이다(G29).

## 상태

- 2026-09-15 — `/mccp:prp-implement` 를 Phase 2.5(implement-codex 게이트)까지 진행하고 receipt 를 쓴 뒤 멈췄다.
  **코드 변경 0.** 구현은 새 세션에서 한다 (사용자 지시).
- 같은 날 사용자 결정 — 아래 「계획서대로는 서지 않는 자리」 1~3 을 보고받고 **재계획(`-m1r`) 대신 현 계획으로
  구현하고 이탈을 공시**하기로 했다.
- 2026-09-15 구현 세션 — Task 1~9 구현. 보조 게이트 `SUPP ALL OK`, 동결 게이트는 `#10` 에서 붉음(예정). plan-conflict
  탐지로 자동 연쇄 중단 — **커밋 전**, 사용자 검토 대기. 원본 덤프 zip 2개는 워크트리 밖 `/home/madsc/work/ops-drive-dump/` 로 옮겼다.
- 같은 날 — 사용자 검토로 `/mccp:code-review` 로컬 모드를 돌려 6건(HIGH 1 · MEDIUM 2 · LOW 3)을 받았고, 사용자가 전건 수용 ·
  커밋을 지시했다. 구현은 리뷰본 그대로 `80e5e6c` 로 봉인하고 흡수는 다음 커밋으로 분리했다. 아래 「로컬 코드 리뷰」.

## 착수 실측 (계획서 GROUND 보강)

| # | 관측 | 명령 · 출력 |
|---|---|---|
| G29 | 계획서 끝에 절 하나를 붙이면 plan hash 가 바뀐다 | `receipt/cli.js hash-plan` 원본 `sha256:d33f14cc…` · 사본 `sha256:40d0dbf8…` |
| G30 | **`#10` 은 BASE golden 이 낡아 통과할 수 없다** | `a8b3904` golden 은 자식 병합 전 판(report `golden_only_in_golden 30` · `golden_only_in_tree 67`). 현재 트리 파일 키 72 중 값이 다른 키 62, 그중 허용 집합 밖 58 (해시 변경 22 · 신규 파일 36) — 전부 M1 이전 변경 |
| G31 | **세 번째 parsed xlsx 는 셀 층 게시를 막는다** | `git archive HEAD` 사본에 `i1-modreq` 행 추가: `extract-cells --report` rc 0 · 게시 rc 1 `OPS_INV_CELL_DIGEST_INDEPENDENT` ← `OPS_INV_CHECK_INTERNAL loss census 가 22행이 아니다: 30` · 21/21 롤백. 원인 `ops/cells/check-cells.mjs:1699` 의 문서 2건 가정 |
| G32 | 비-xlsx parsed 행이 들어오면 셀 추출기 계수 대조가 어긋난다 | `ops/cells/extract-cells.mjs:1054` 가 xlsx parsed 행 수를 `lock.counts.parsed`(전 kind)와 비교 → `OPS_INV_LOCK_COUNT_DRIFT` (코드 판독 · 미실행) |
| G33 | 사유 문서에 이름 열이 있다 | `수정사항관리대장` row 5 헤더 `작성자`(I) · `담당자`(K), 각 distinct 2 · 한글 2~4자. row 3 D 에 `수정 담당자`. `수정요청자료` 시트는 날짜 · 드라이브 링크 2행 |
| G34 | 셀 층 독립 검사기는 `v` 를 원본에서 따로 읽어 대조한다 | `ops/cells/check-cells.mjs:543` `CK_DIGEST_FIELDS` 에 `v` · `:872` `runDigest` — 추출기에서만 가리면 `extract-cells --check` 가 digest 로 깨진다 |
| G35 | 셀 추출기 자기시험은 원본 루트를 요구한다 | `extract-cells --selftest` env 없이 rc 1 `OPS_INV_SELFTEST_NO_SOURCE` (`ops/cells/extract-cells.mjs:3926`), env 주면 rc 0 `selftest_fixtures 20`. gates.sh `#3` 은 env 를 export 해서 불러야 선다: `OPS_SOURCES_ROOT=.samples bash .claude/plans/ops-doc-reconcile-intake-m1.gates.sh` |
| G36 | 착수 기준선 | `extract-cells --check` rc 0 · `check-statements --check` rc 0 `anchor_cells_checked 316` · `bash ops/statements/_harness/m0.sh reverify` rc 0 (7.7s) · verify-ops report `reproduce_ran cells rc=1 · db rc=1 · reconcile rc=0 · statements rc=0` |
| G37 | 관측 도구가 설치됐다 (G9 해소) | `tesseract 5.3.4` (leptonica 1.82.0) · langs `eng kor osd` · `pdftotext version 24.02.0` |
| G38 | 표본 후보가 G8 실측과 같은 파일이다 | zip 내부 재측정: pptx `02. 개발 2차/01. 기획/02. 관리자 매뉴얼/보험유니버스_관리자 매뉴얼.pptx` slides 132 · `a:t` 3691 · media 118 / hwpx `01. 개발 1차/02. 기획/01. 약관-TEXT문서/보험금청구동의서 약관/06. 보험금 청구 동의서 약관.hwpx`(bak 아님) `hp:t` 81 / docx `04. 개발 4차/01. 기획/02. B2C 약관문서/보험유니버스_개인정보 제3자 제공 동의_확정.docx` `w:t` 48 / 사유 xlsx sha256 `69d204de…`(G5) / manifest 원본 3건 sha256 앞자리 `26d5bd03`(계정 리스트) · `48a1b360`(검색 색상) · `5a3f431c`(기업로그인) |
| G39 | 이미지 후보의 OCR 미리보기 | 운영 공지 모달 `01. 개발 1차/05. 디자인/03. 모달 이미지/보험유니버스_서비스 일시 중단 안내_팝업 이미지.png` 가 중단일시 · 중단사유 · 중단업무 줄을 온전히 읽는다(`scope`·`decision` 진술 후보). 사유 xlsx 내장 이미지 2건은 서비스 화면 캡처. **사람 육안 판정 전** |
| G40 | 설계 게이트 | `impeccable-detect --mode implement` → `skill_available true · design_signal false · silent_skip no-signal` |
| G41 | 사유 xlsx 코멘트 작성자 경로 | `xl/comments*.xml` 0건 — 작성자 이름이 `comment.author` 로 새는 경로는 이 문서에 없다(기존 `s1-simwon-items` 는 코멘트 5건에 작성자가 실린다) |
| G42 | **`#10` 은 golden 형식을 잘못 읽는다** (Codex R1 F1 반사실) | gates.sh `:284-320` 원문을 합성 golden(생산자 형식 그대로 `reproduce_ran statements rc=0→rc=1` 퇴행 + `reproduce_ran intake rc=1` 추가)에 돌림 → `repro_regressed=0 intake_axis=0 golden_outside=1(reproduce_ran)` rc 1. golden 본문은 `reproduce_ran <축> rc=N` 무번호인데 `#10` 은 `reproduce_ran.N` 만 센다 — 퇴행 · intake 축 · `missing_sources` · `reproduce_unavailable` 판정이 전부 공허하다 |
| G43 | 체커 단독 복사 하네스는 G27 하나가 아니다 | `ops/reconcile/_harness/m0.sh:173` · `:239` · `ops/reconcile/_harness/m1.sh:187` · `ops/reconcile/_harness/m5.sh:480` 도 `check-statements.mjs` 만 복사한다 (L2 R2 architect LOW) |

## 계획서대로는 서지 않는 자리

1. **`#10`** — 낡은 BASE golden(G30)과 형식 오독(G42) 두 겹으로 통과 불가. gates.sh 는 계획서가 sha 로 핀했다.
2. **사유 문서 xlsx 를 셀 층으로 넣는 경로** — 셀 층 검사기가 문서 2건을 가정한다(G31). 필요한 변경이 Files to Change 밖
   (`ops/cells/check-cells.mjs` · `ops/findings/ops-10-cells.md`)이다.
3. **이름 열 가림** — 추출기에서만 가리면 독립 검사기와 어긋난다(G33 · G34).
4. 셀 추출기 계수 대조(G32) — 파일은 범위 안이지만 계획서가 이 수정을 적지 않았다.

## 이탈 결정 (구현 세션이 따른다)

1. implement 리뷰 절과 편차 기록은 이 notes 에 둔다 (G29).
2. 범위 확장 2파일 — `ops/cells/check-cells.mjs`(loss census 행 수를 `8×문서수+6` 으로, 가림 규칙 문서의 재도출 행에 같은 가림
   함수 적용) · `ops/findings/ops-10-cells.md`(census 블록 재도출). PR 본문에 공시한다.
3. **보조 실행 게이트를 파일로 둔다** — `.claude/plans/ops-doc-reconcile-intake-m1.gates-supp.sh` (Codex R1 F1 · F2). notes 산문은
   실행 정책이 아니다. 요건:
   1. golden 무번호 키(`reproduce_ran` · `missing_sources` · `reproduce_unavailable`)의 **값 전건**을 파싱해 intake 축 존재 ·
      BASE 에서 rc 0 이던 축의 퇴행 · 잔여 0 을 문다.
   2. 허용 집합 = 계획서 Files to Change + 워크플로 파일(계획서 · gates.sh · 이 notes · 보조 게이트 자신 · backlog · 리뷰 기록 ·
      이 slug receipt) + 신규 parsed id 의 `ops/media/<id>/` + **이탈 2의 2파일**. golden 파일 키는 BASE 대비 내용이 바뀐
      파일만 대조한다(G30 의 M1 이전 변경 제외).
   3. 동결 `#10` 뒤의 검사(`#11`)를 독립 실행한다 — 동결 게이트가 `#10` 에서 끝나도 나머지가 건너뛰어지지 않게.
   4. 음성 fixture: 재현 축 퇴행 · intake 축 부재 · 허용 밖 파일 변경 각각이 rc 1 임을 게이트가 스스로 보인다.
   5. L2 R2 인수분(아래 표)을 같은 파일에서 돈다.
4. gates.sh 는 바이트 불변으로 두고 `OPS_SOURCES_ROOT=.samples` 를 export 해 1회 완주한다. `#10` 에서 붉게 끝나는 출력을
   그대로 여기 붙이고, 판정은 보조 게이트 출력으로 한다.

## 구현 결정 (Codex R1 에 올린 판)

1. 리뷰 절 · 편차 → 이 notes.
2. gates.sh 바이트 불변 + 보조 검사 (R1 뒤 「이탈 결정 3」 으로 실행 파일화).
3. 범위 확장 2파일 (이탈 결정 2).
4. **가림 소유자는 하나** — `ops/intake/extract-intake.mjs` 의 export 순수 함수. `ops/intake/mask-rules.json` 에 실린 문서에만 건다.
   1단계 정본 PII 패턴 스팬 → `[PII:<종류>]`, 2단계 그 뒤에도 `piiHit`(NFKC)가 무는 값은 값 전체 → `[PII:<종류>]`, 이름 열은
   행 우선 첫 등장 순서로 `person-<규칙접두><n>`. 가명은 원본 바이트의 순수 함수라 추출기와 검사기가 공유 상태 없이 일치한다.
   저장소 밖 매핑 파일은 사람용 기록이며 게시 모드만 규칙 키 단위로 병합해 쓴다. 가림 함수 자체는 독립 검증이 없다는 대가를
   직접 대조(보조 게이트)로 막는다.
5. `ops/cells/extract-cells.mjs` — `main()` 에 `IS_ENTRY` 가드 · `openZip(buf, allow = entryAllowed)` 로 화이트리스트를 zip 객체에
   실어 intake 가 같은 예산 · CRC · 압축비 검사 아래 자기 화이트리스트로 docx/pptx/hwpx 를 연다 · `openZip` · `zipReadBytes` ·
   `parseXml` · `decodeEntities` export · 계수 대조를 xlsx 행으로 한정(G32).
6. `extract-intake.mjs` 는 셀 추출기와 층 2 체커를 함수 안 동적 import 로 적재한다 — 셀 검사기가 가림 함수를 import 해도
   추출기 리더가 모듈 수준에서 올라오지 않는다.
7. 관측 형상은 층 1 A1 행 `{doc,sheet,ref,row,col,v,t}` 재사용: docx `body` = 비지 않은 `w:p` 문서 순서(표 안 문단 포함) ·
   pptx `s<N>` = 슬라이드 **파일 번호 순**(presentation.xml 순서 아님) · hwpx `sec<N>` `hp:p` · pdf `pdftotext -layout` 을
   form feed 로 쪼갠 `p<N>` 의 비지 않은 줄(오른쪽 공백 제거) · 이미지 `tesseract … -l kor+eng` stdout(`OMP_THREAD_LIMIT=1`) `img`.
   `v` 문자열 · `t` `"s"`. 원본 루트는 `OPS_SOURCES_ROOT` 만 읽는다.
8. `ops/statements/check-statements.mjs` — 층 1 파일 목록 수집을 한 함수로 두고 `ops/cells` · `ops/intake` 를 훑는다. 교차
   디렉터리 중복 id 는 `readCells` 가 `OPS_INV_CELL_ROW_INCONSISTENT` 로 죽고 `censusCells` 는 던진다(프로세스 종료 아님).
   `ops/intake` 부재는 빈 목록(하네스가 체커만 복사 — G27 · G43). `PII_PATTERNS` · `piiHit` export, import 추가 없음.
9. `ops/_meta/sources.lock` 17행 재생성(디스크 실재 기존 9 + 신규 8), 파생 csv/txt 38행 제거. Drive 덤프 zip 은 워크트리 밖으로.
10. tesseract `kor.traineddata` 는 해시 핀하지 않는다(바이너리 버전만) — 알려진 상한.

## L2 R2 지적 인수 (backlog → 구현)

| backlog id | 처리 |
|---|---|
| `odr-intake-m1-l2-harness-not-run` | 보조 게이트가 `bash ops/statements/_harness/m0.sh reverify` 를 돈다. 리더 변경 회귀용으로 statements `m1`~`m4` · reconcile `m0`~`m5` 의 `reverify` 도 함께 |
| `odr-intake-m1-cross-dir-dup-negative-absent` | 보조 게이트 음성: 사본에 `ops/intake/s1-simwon-items.jsonl` 을 복제 → `check-statements --check` rc 1 · `OPS_INV_CELL_ROW_INCONSISTENT`, `--census` 도 실패 |
| `odr-intake-m1-mask-probe-self-report` | 보조 게이트가 export 가림 함수에 합성 입력(전각 전화번호 · 이름 열)을 넣어 출력을 직접 대조 |
| `odr-intake-m1-mask-hook-two-owners` | 구현 결정 4 — 소유자 하나 · 가명은 원본의 순수 함수 · 셀 검사기 재도출에도 같은 함수 |
| `odr-intake-m1-ocr-empty-formfeed` | 보조 게이트는 공백 · form feed 제거 뒤 길이로 빈 출력을 센다. 빈 PNG 1장으로 tesseract 5.3.4 의 `.txt` 가 form feed 를 쓰는지 실측해 여기 기록 |
| `odr-intake-m1-g27-undercounts-harnesses` | G43 으로 정정 |
| `odr-intake-m1-census-dup-id` | 구현 결정 8 — `censusCells` 도 충돌을 던진다 |
| `odr-intake-m1-accepted-pii-residue` | 기각 유지 (계획서 Risks · Acceptance `(미검증)`) |

## Task 1 사람 판정에 올릴 것

- 신규 표본 전건 육안 판정 → lock `sensitive_review` · 각 행 `sensitive_reviewed_sha256`. 사유 xlsx 는 이름 열이 있으므로(G33)
  판정문이 「개인 식별 정보 0건」 이 아니라 「이름 열 있음 — 관측 시 가림」 이어야 한다. 내장 이미지 2건도 본다.
- 파일명이 PII 인 원본의 이름을 `.claude/state/ops-intake/pseudonyms.json` 의 `pii_names` 에 사람이 적는다(G6). 이 파일 ·
  계획서 · findings 어디에도 그 이름을 적지 않는다(`#9` 가 착수 이후 바뀐 파일 내용을 훑는다).

## 구현 기록 — 2026-09-15 구현 세션

### 착수 뒤 사용자 결정

1. **사유 xlsx 는 intake 가 셀 값만 관측한다** — 내장 이미지 1건에 로그인 사용자 실명이 있다(G45). 계획 원판(셀 층 +
   `ops/media/i1-modreq/`)을 버리고 lock 행에 `observer: "intake"` 를 싣는다. 셀 층 산출 · ops-10 census 22행 가정 ·
   독립 검사기 가림 문제(G31 · G34)가 함께 사라지고, 범위 확장은 `ops/cells/check-cells.mjs` 의 소스 술어 한 자리만
   남는다. `ops/findings/ops-10-cells.md` 는 바뀌지 않는다.
2. **pptx 작성자 실명은 좌표 지정 가명**(G44) — `ops/intake/mask-rules.json` `i3-manual` 의 `s1!A9` · `s2!A9` → `person-p1`.
3. **PII 판정은 사전 판독대로 lock 에 기록** — `sensitive_review.intake_m1` 과 새 행 8건의 `sensitive_note`.

### 착수 뒤 실측

| # | 관측 | 명령 · 출력 |
|---|---|---|
| G44 | pptx 표지와 개정 이력에 작성자 실명이 있다 | 슬라이드 문단 1,733 중 그 이름이 나오는 문단 2곳(`s1!A9` · `s2!A9`). 패턴 3종 적중 0 |
| G45 | 사유 xlsx 내장 이미지 1건에 실명이 있다 | `xl/media/` 2건 중 모바일 화면 캡처의 로그인 인사말(육안). 다른 1건은 주의사항 팝업 캡처 |
| G46 | 계정 리스트 xlsx 에 자격증명이 있다 | `심평원API` 시트 ID/PW · `서비스계정LIST` 시트. unparsed 등재만. 판독 중 값 한 줄이 세션 터미널에 찍혔고 파일에는 안 적혔다 |
| G47 | 빈 이미지 OCR 은 0바이트다 | 120×120 흰 PNG → tesseract 5.3.4 `.txt` 0바이트(form feed 없음). `ops/media` 16건 OCR 실패 0 · 빈 출력 0 · 패턴 적중 0 |
| G48 | 사유 xlsx 이름 토큰은 I · K 밖에 안 나온다 | 6행 이상 I · K 의 토큰 5개, 다른 셀 등장 0 |
| G49 | 하네스 reverify 11건 중 붉음 7 | statements `m0` `COUNTER_MISSING [cells_docs_expected 2]`(lock parsed 2→8) · reconcile `m1` `COUNTER_MISSING locality [locality_stmt_rows 143]`(진술 143→157) · `m5` `UNEXPECTED_RED`(m1 붉음 사유가 바뀜) — 셋은 M1 이 옮긴 박힌 계수다. reconcile `m0` · `m2` · `m3` · `m4` 는 `TOKEN_SET_SIZE check=21 (20 이어야 한다)` — `ops/reconcile/` 는 `a8b3904` 이후 무변경이고 `reconcile.mjs --list-tokens` 의 check 가 21 이라 착수 전부터 붉다. statements `m1`~`m4` 는 초록 |
| G50 | 파일명 PII 후보를 폴더 전체로 채운 것은 틀렸다 | `수정사항 원자료/` 최상위 2건은 lock 기존 원본(`s1-customer-test` · `s1-addl-holdings`)이라 동결 `#9` 가 `content_hits=2` 로 멈췄다. `241121_…/` 하위 7건과 그 폴더명 8건으로 좁힌 뒤 `content_hits=0` |

### 계획 · 앞 세션 구현 결정 대비 편차

1. 가림 소유자는 `ops/intake/extract-intake.mjs` 하나이고 **셀 검사기는 가림 함수를 import 하지 않는다** — 결정 1 로 사유
   xlsx 가 셀 층을 떠나 필요가 사라졌다(구현 결정 4 · 6 의 검사기 쪽 절 폐기).
2. `extract-intake.mjs` 는 층 2 체커를 **정적** import 한다(구현 결정 6 은 함수 안 동적 import). 가림 함수가 동기여야 하고
   체커는 `IS_ENTRY` 가드라 부작용이 없다(동결 `#12` · 보조 `#S5`). 셀 추출기는 함수 안 동적 import 그대로다.
3. `extract-cells.mjs` 에 가림 훅을 넣지 않았다(Files to Change Why 열의 "가림 규칙 소스에서만 동적 import") — 셀 층에 가림
   대상 문서가 없다. 대신 소스 술어 `isCellsSource` 를 7자리에 걸었고, 계수 대조를 kind 무관 parsed 행과 한다(G32).
4. `ops/cells/i1-modreq.jsonl` · `ops/media/i1-modreq/` 대신 `ops/intake/i1-modreq.jsonl`. xlsx 관측 행은 값 있는 셀만
   `{doc,sheet,ref,row,col,v,t}` 로 내고, 병합 비좌상단 · 빈 셀 · 채움 · 코멘트 · 이미지는 안 낸다.
5. 가림 규칙은 좌표로 고른 셀의 **이름 토큰**을 문서 전 행에서 치환한다. G48 로 전파 0 이지만 정규식 치환 한 번이다.
6. 관측기가 `pseudonym_map` 에 쓰기 전에 `git check-ignore` 로 확인한다(`writeNames`).
7. 보조 게이트 허용 집합: 계획 Files to Change − `ops/cells/i1-modreq.jsonl` + 워크플로 파일 + `ops/cells/check-cells.mjs` ·
   `ops/intake/i1-modreq.jsonl` · 구현 보고서. `ops/findings/ops-10-cells.md` 는 안 넣었다(안 바뀐다).
8. reconcile 하네스 reverify 는 회귀망이 아니다(G49). 보조 게이트는 하네스 결과를 원장과 대조하고(`#S10`), 리더 회귀는
   m0 fixture case 전건을 착수 커밋 체커와 현 체커로 돌려 바이트 대조한다(`#S11`).
9. 이미지 표본 id `i6-shot` 은 계획 표 그대로다(i5 결번 — pdf 표본은 기존 `s1-addl-holdings`).

### 게이트 완주 (2026-09-15 최종 트리)

동결 게이트 — `OPS_SOURCES_ROOT=.samples bash .claude/plans/ops-doc-reconcile-intake-m1.gates.sh` rc 1. `#0`~`#9` · `#12` 통과, `#10` 에서
멈춘다(G30 · G42 · 이탈 3파일). 동결 본문이 `set -e` 라 `#11` 은 여기서 돌지 않고 보조 `#S2` 가 돈다.

```text
tools_ok pdftotext=24.02.0 tesseract=5.3.4
intake_docs 6
intake_pii_masked 0
intake_rows.i1-modreq 196
intake_rows.i2-consent 17
intake_rows.i3-manual 1733
intake_rows.i4-claim-terms 81
intake_rows.i6-shot 8
intake_rows.s1-addl-holdings 35
intake_pseudonyms.i1-modreq 5
intake_pseudonyms.i3-manual 1
intake_check ok
intake_docs 6
intake_pii_masked 0
intake_rows.i1-modreq 196
intake_rows.i2-consent 17
intake_rows.i3-manual 1733
intake_rows.i4-claim-terms 81
intake_rows.i6-shot 8
intake_rows.s1-addl-holdings 35
intake_pseudonyms.i1-modreq 5
intake_pseudonyms.i3-manual 1
intake_check ok
coverage docx=17 hwpx=81 image=8 pdf=35 pptx=1733 xlsx=196
anchors total=335 base=316 fresh=19
axes decision=3 history=6 scope=2
pii mode=jsonl scanned=2049 hits=0 masked_tokens=0
media files=16 ocr_fail=0 ocr_empty=0
pii mode=text scanned=16 hits=0 masked_tokens=0
staging new_rows=8 bad_paths=0 pii_names=8 changed_files=27 content_hits=0
scope changed=27 outside=3 golden_keys=84 golden_outside=62 repro_regressed=0 intake_axis=0 repro_unavailable=0
-- stderr
OPS_M1_SCOPE 범위 밖=.claude/plans/ops-doc-reconcile-intake-m1.gates-supp.sh,ops/cells/check-cells.mjs,ops/intake/i1-modreq.jsonl golden 범위 밖=missing_sources,reproduce_ran,reproduce_unavailable,ops/cells/spike-m0-style-axis.mjs,ops/cells/spike-m0-style-axis.txt 재현 퇴행= intake_axis=0 missing_sources 잔여=0 reproduce_unavailable 잔여=0 golden_keys=84
FAIL #10 변경 범위·golden
```

보조 게이트 — `OPS_SOURCES_ROOT=.samples bash .claude/plans/ops-doc-reconcile-intake-m1.gates-supp.sh` rc 0. **판정은 이 출력으로 한다.**

```text
scope changed=27 outside=0 golden_file_keys=15 golden_outside=0 golden_stale_excluded=57 repro_regressed=0 intake_axis=1 missing_sources=0 repro_unavailable=0
scope_negatives 3/3
prd m1_status=in-progress m1_plan=`.claude/plans/ops-doc-reconcile-intake-m1.plan.md` exempt_rows=8
deviation observer=intake cells_artifact=0 media_dir=0 intake_artifact=1 cells_layer_sources=2 cell_files=2
deviation_negative caught
mask_direct cases=8 ok
mask_rules_negative rc=1 token=OPS_INV_MASK_RULES
imports clean=3
imports_negative base_extract_cells rc=1
dup positive_check=0 dup_check=1 token=OPS_INV_CELL_ROW_INCONSISTENT dup_census=1
zip plain=0 rows 1 encrypted=1 token=OPS_INV_ZIP_ENCRYPTED
names docs=2 tokens=6 scanned=27 hits=0 negative_hits=1
ocr_blank bytes=0 stripped=0
harness total=11 green=4 known_red=7 unexpected=0
fixtures cases=95 identical=95
fixtures_negative mutated_differs=5/5
SUPP ALL OK
```

### 마감 중 발견

1. **G51 `extract-intake.mjs` 에 NUL 2바이트** — 좌표 키 구분자로 적은 유니코드 이스케이프가 도구 입력에서 원문자로 풀려 저장됐다.
   런타임 값은 같아 게이트는 통과했지만 `git diff --numstat` 이 바이너리로 세고 grep 이 줄을 안 냈다. 이스케이프 6글자로 바꾸고
   golden 재갱신 · 두 게이트 재완주(위 출력이 그 뒤다).
2. **plan-conflict 탐지기 `file-expansion`** — `origin/main...HEAD` 기준 계획 밖 254파일(축 브랜치 누적), BASE `a8b3904` 기준 9파일
   (워크플로 파일 · 보조 게이트 · 이탈 2파일). 명령 본문대로 fix-task `plan_conflict` · STATE `chain_aborted` · receipt
   `plan_conflict_escalated` 를 남겼다. 커밋 · PR 자동 연쇄는 돌지 않는다 — 사용자 검토 뒤 결정.
3. 구현 보고서(`.claude/PRPs/reports/…-report.md`)는 쓰지 않았다 — 명령 본문이 plan-conflict 에서 Phase 5 진입을 막는다. 이 notes 가 기록이다.

## Codex Implementation Review

- 호출: `node ${CLAUDE_PLUGIN_ROOT}/scripts/lib/codex-invoke.js adversarial-review --base a8b3904` (fail-closed Bash wrapper, v0.2.2 · mccp 1.33.6)
- 라운드 수: 1 (원장 `mccp-implement-codex__ops-doc-reconcile-intake-m1` 1/3)
- 합치 결론: `needs-attention` → 게이트 판정 `divergent`(구조화). 제안된 검증 근거로는 출하할 수 없다 — `#10` 이 golden 형식을
  잘못 읽고, 보조 검사는 산문이 아니라 범위 확장을 반영한 실행 파일이어야 한다. 두 지적 모두 구현 결정으로 흡수했고
  (이탈 결정 3), 실행 증명은 구현 세션 몫이라 판정은 `divergent` 로 봉인한다.
- YAGNI Triage:
  | Finding | Severity | Verdict | Why |
  |---|---|---|---|
  | F1 `#10` 이 생산자가 내지 않는 번호 키를 파싱한다 (`.claude/plans/ops-doc-reconcile-intake-m1.gates.sh:307`) | HIGH | ACCEPT_NOW | 반사실 확인(G42) — 퇴행 · intake 축 · 잔여 판정이 공허하다. 보조 게이트가 무번호 키 값을 파싱하고 음성 fixture 로 문다 (이탈 결정 3-1 · 3-4) |
  | F2 보조 범위 검사가 범위 확장 2파일과 `#11` 독립 실행을 담아야 한다 (`.claude/plans/ops-doc-reconcile-intake-m1.gates.sh:286`) | MEDIUM | ACCEPT_NOW | 동결 허용 집합으로는 이탈 결정 2 가 늘 범위 밖이고, `set -e` 게이트가 `#10` 에서 끝나 `#11` 이 안 돈다 (이탈 결정 3-2 · 3-3) |
- R2 미발화: ACCEPT_NOW HIGH 1건은 결정 수준에서 해소됐고(보조 게이트 요건 확정), 남은 것은 코드 증명이라 이 세션에서 더 물을
  대상이 없다. 구현 뒤 PR-Codex 가 실제 diff 를 본다.
- Deferred to backlog: 0
- Open Questions: 없음 (§0 auto-CRITICAL 해당 없음)
- Codex session 참조: codex-invoke `classification=ok` · `durationMs=83007` (task id 미노출)

### Design Review

> impeccable silent-skip: `skill_available=true · design_signal=false · reason=no-signal` — 렌더 표면이 없는 관측 파이프라인이다 (G40).

### Security Reviewer

`mccp:security-reviewer` 1회 (설계 대상 · 코드 0). 주장은 원문 대조 뒤 판정했다.

| Finding | Severity (주장) | Verdict | 대조 · 처리 |
|---|---|---|---|
| S1 `extract-intake.mjs` 가 셀 검사기에 import 되는데 `IS_ENTRY` 요구와 `#12` 는 두 모듈만 본다 | CRITICAL | ACCEPT_NOW | 사실 — 구현 결정 6 이 import 경로를 새로 만든다. **결정 추가**: `extract-intake.mjs` 도 `IS_ENTRY` 가드, 보조 게이트가 세 모듈 import 부작용 0 을 문다 |
| S2 `ops/cells/i1-modreq.jsonl` 은 `piiHit` 전수 훑기 밖이고 `comment.author` 가 가림 설계 밖이다 | CRITICAL | 부분 기각 · ACCEPT_NOW(가림 범위) | 앞 절 거짓: gates.sh `#7` 은 신규 id 마다 `ops/cells` · `ops/intake` 둘 다 훑는다(`.claude/plans/ops-doc-reconcile-intake-m1.gates.sh:213-214`). 코멘트 작성자 경로는 이 문서에 코멘트 파트 0건(G41). **결정 보강**: 가림 대상 문서의 모든 문자열 필드(`v` · `comment.author` · `comment.text`)에 가림 함수를 건다 — 비용이 작고 다음 문서에서 열린다 |
| S3 화이트리스트 매개화 시 `openZip` 구조 검사가 전역 `entryAllowed` 에 남으면 docx/pptx/hwpx 엔트리가 ZIP64 · 암호화 · 선언 크기 검사를 건너뛴다 | HIGH | ACCEPT_NOW | 사실 — 구조 검사가 `if (entryAllowed(name))` 에 걸려 있다(`ops/cells/extract-cells.mjs:169`), `zipReadBytes` 도 같은 전역을 부른다(`:209`). **결정 명시**: 두 지점을 한 변경에서 `zip.allow` 로 바꾸고, 보조 게이트가 docx 화이트리스트로 암호화 플래그 엔트리를 연 합성 zip 이 `OPS_INV_ZIP_ENCRYPTED` 로 죽는지 본다 |
| S4 외부 도구 인자 주입(`-` 로 시작하는 경로) · 심볼릭 링크 | MEDIUM | ACCEPT_NOW | `path.resolve` 절대 경로를 넘기면 `-` 로 시작할 수 없다. 소스 루트 이탈 가드(`ops/cells/extract-cells.mjs:1080-1085`)를 그대로 따르고 `lstat` 로 링크를 거부한다(verify-ops 도 소스 루트 링크를 `SYMLINK` 로 거부) |
| S5 pdf · 이미지 입력 크기 상한 없음 | MEDIUM | REJECT_YAGNI | 표본은 사람 판정을 거친 형식당 1건이다. 전건 관측(M3)으로 넓힐 때 `tools.lock` 에 상한을 둔다 |

미해소 CRITICAL · HIGH 0 — receipt 를 쓴다.

## 로컬 코드 리뷰 (2026-09-15, 리뷰본 `80e5e6c`)

`/mccp:code-review` 로컬 모드 1회. 사용자가 전건 수용했다. 흡수마다 흡수 전 판(`80e5e6c`)이나 변형으로 결과가 뒤집히는지 보조
게이트 `#S12` 가 매번 재현한다.

| # | 지적 | 심각도 | 흡수 | 반사실 (`#S12` 출력) |
|---|---|---|---|---|
| 1 | `loadValidatedLockAliases` 가 셀 층 행 수(2)를 `counts.parsed`(8)와 댄다 — G32 를 `loadParsedSources` 한 자리만 고쳐 `--sweep-orphans` · `--verdict-probe` 가 늘 `OPS_INV_LOCK_COUNT_DRIFT` | HIGH | 계수만 kind 무관 parsed 행과 대고 별칭 집합은 셀 층 행 그대로 | `lock_count=OPS_INV_USAGE` · 흡수 전 판 `pre=OPS_INV_LOCK_COUNT_DRIFT` · 계수 -1 lock `negative=OPS_INV_LOCK_COUNT_DRIFT` |
| 2 | `IS_ENTRY` 가 `path.resolve(argv[1])` 를 링크를 푼 `import.meta.url` 과 대서 링크 경로 실행이 인자와 무관하게 조용히 rc 0 — `extract-cells` 는 원래 무조건 돌았으므로 퇴행 | MEDIUM | 세 엔트리(`extract-cells` · `extract-intake` · `check-statements`) 모두 양쪽 실경로 비교 | `entry_link rc=1,1,2 pre=0,0,0` |
| 3 | 문단 관측이 `w:br` · `w:cr` · `a:br` · `hp:lineBreak` · 글 안 탭을 버려 앞뒤 낱말이 붙는다 — 이름 규칙 토큰이 합쳐지면 다른 행의 단독 이름을 못 가린다 | MEDIUM | 공백 1칸. 탭은 부모가 `r` · `t` 일 때만(문단 속성 탭 정지 제외). observe_probe 에 줄바꿈 · 탭 정지 대조 추가, 산출 재생성 — `i2-consent` A1 1행 +1바이트, 이 문서 진술 앵커(A13) 무영향 | `observe_probe ok mutants=2/2` (줄바꿈 제거 · 탭 무조건 변형 둘 다 `OPS_INV_INTAKE_SELFTEST`) |
| 4 | 가명 매핑 파일이 기본 권한 644 | LOW | `mode 0o600` + 기존 파일 `chmod` | `map_mode=600` (흡수 전 644 실측) |
| 5 | `readSource` 가 `lstat` 로 마지막 성분만 봐 루트 아래 중간 디렉터리 링크를 따라간다 — 주석 「링크는 따라가지 않는다」와 어긋남 | LOW | 루트 기준 상대 경로가 실경로 전후로 같아야 한다(루트 자체 링크는 허용). `readSource` export | `source_link plain=0 dir_link=1 file_link=1 root_link=0 mutant_dir_link=0` |
| 6 | 한 문서에 열 규칙이 여럿이면 한 규칙이 0셀을 골라도 다른 규칙이 고르면 통과 | LOW | 열 규칙마다 0셀이면 `OPS_INV_MASK_RULES` | `mask_cols rc=1 pre=0` |

부수 변경:
- 보조 게이트 `#S11` 전제가 `git diff BASE HEAD` 였다 — 구현을 커밋하면 HEAD 가 M1 변경을 담아 늘 실패한다. 대조 기준을 구현 직전 커밋
  `PRE_IMPL=e9f9514` 로 핀했다.
- golden 4행 재생성(`verify-ops.sh --update`) — `extract-cells.mjs` · `extract-intake.mjs` · `i2-consent.jsonl` · `check-statements.mjs`.
  `verify-ops.sh` 리포트 모드는 golden 불일치에도 rc 0 이고 `verdict fail` 행으로만 알린다(설계 — 자기시험 (y)).

게이트 (흡수 뒤 트리, `OPS_SOURCES_ROOT=.samples`): 보조 게이트 `SUPP ALL OK` (`#S0`~`#S12`) · `verify-ops.sh` `verdict ok` ·
`extract-intake --check` · `extract-cells --check` · `check-statements --check` · `--census` 전부 rc 0.

리뷰가 짚었으나 흡수 대상이 아닌 것: implement receipt 가 `codex_verdict: divergent` 와 `resolution.converged: true` 를 함께 싣는다.
receipt CLI 가 쓴 파일이라 손으로 고치면 `receipt_hash` 가 깨진다 — 손대지 않았다.

## Milestone Closure (2026-09-15)

`/mccp:milestone-close` — 종결 문서 `.claude/milestone-closures/1-m1-intake-samples.md` (`goal-done`, 판정 위임), PRD 마일스톤 1행
`complete` · 스탬프는 PRD `## Milestone Closure Provenance` 표. 실측 대상 `f73182f` 출력은 종결 문서에 있다.

보조 게이트 `#S1` 허용 집합에 종결 문서 경로를 넣었다 — 반사실: 넣기 전 `scope changed=28 outside=1` ·
`FAIL #S1` (범위 밖=`.claude/milestone-closures/1-m1-intake-samples.md`), 넣은 뒤 아래.

종결 트리 재실측 (커밋 전 워킹트리, `OPS_SOURCES_ROOT=.samples`): 보조 게이트 rc 0 · stderr 0바이트 · `SUPP ALL OK`.
`f73182f` 출력 대비 다른 줄은 셋이다 — `scope changed=28`(+종결 문서) · `names … scanned=28 hits=0` · `prd m1_status=complete`.
`hash-plan` `sha256:d33f14cc…` 불변, `validate --command mccp:pr … --plan` `ok:true` · `stale []` · `blocking []`.
