# ops-30-reconcile M0 — 구현 기록

계획: `.claude/plans/ops-30-reconcile.plan.md` (M0 — 스키마 + 해시 규칙)

리뷰 절은 계획서가 아니라 이 파일에 적는다. 계획 본문을 고치면 `mccp-plan-codex` receipt 의
`plan_hash` 가 어긋나 게이트 사슬이 stale 로 떨어진다.

## Codex Implementation Review

- 호출: `node C:/Users/madsc/.claude/plugins/cache/mccp/mccp/1.29.0/scripts/lib/codex-invoke.js adversarial-review` (fail-closed Bash wrapper, v0.2.2)
- 라운드 수: **2** — R1(게이트 순서상 빈 diff) + R2(구현 후 산출물 경로 명시).
  `MCCP_REVIEW_SINGLE_PASS=deadline_pressure` 로 gate 의 escalation cap 은 1 이지만, R1 이 아무것도
  리뷰하지 못했으므로 Phase 3 완주 뒤 실물 리뷰를 한 번 더 돌리고 그 결과를 아래 §R2 에 적었다.
- 합치 결론: **R1 은 구현 결정을 리뷰하지 못했다.** 게이트가 Phase 3 EXECUTE 앞에 있으므로
  Codex 가 본 워킹트리에는 `ops/reconcile/**` 산출물이 하나도 없었고, 유일한 지적이
  *"리뷰 대상 파일이 없다"* 였다. 2.5.2 가 낸 구현 결정 7건 중 어느 것도 검증되지 않았다.
- YAGNI Triage:

  | Finding | Severity | Verdict | Why |
  |---|---|---|---|
  | F1 — planned M0 artifacts absent (`ops/reconcile/reconcile.mjs` 등 5건) | HIGH | ACCEPT_NOW | 사실이다. 다만 원인이 구현 결함이 아니라 **게이트 순서**다 — 2.5 는 EXECUTE 이전에 돌고, 그 시점의 diff 는 PRD·plan·receipt 뿐이다. R1 안에서는 해소 불가이고 해소 수단은 Phase 3 완료 뒤 재실행이다 |

- Deferred to backlog: 0 → `.claude/plans/codex-findings-backlog.md`
- Open Questions (R1 시점): **DIVERGENT_UNRESOLVED** — F1 이 round cap(1) 안에서 해소되지 않았다.
  §0 auto-CRITICAL 카탈로그(보안 경계 · 원자적 상태 · 스키마 파손)에는 해당하지 않으므로
  MCCP-GATE-STOP 대상이 아니다. receipt 는 `codex_verdict=divergent` 로 봉인한다.
- Codex session 참조: `01a0190d-ac08-7883-8359-853f75c4bc17`

### R2 — 구현 후 재실행 (산출물 경로 명시)

- 호출: 같은 wrapper. focus 에 `ops/reconcile/reconcile.mjs` · `_harness/m0.sh` · `_harness/golden/**` ·
  `reconcile.jsonl` · `ops/findings/ops-30-reconcile.md` 를 **경로로 열거**하고 검사 축 6개를 지정했다.
- Codex session: `01a0190d-ac08-7883-8359-853f75c4bc17` (R1) · R2 는 같은 wrapper 재호출
- 결과: `verdict=needs-attention`, 지적 **6건** (HIGH 4 · MEDIUM 2). R1 과 달리 전부 실물 코드 지적이다.
- **6건 전건을 코드와 대조했고 전건 사실이었다.** 전건 `ACCEPT_NOW` 로 흡수했다.

| Finding | Severity | Verdict | 흡수 내용 |
|---|---|---|---|
| F1 — 선언된 잎이 구조로 넓혀지면 미선언 키가 해시에 들어간다 + 모르는 `source` 를 comment 로 fallback | HIGH | ACCEPT_NOW | `L` 을 **스칼라 잎**으로 좁히고 열린 subtree 는 `OPEN(사유)` 로 이름을 대게 했다. 미지 source 는 fallback 없이 미지 키로 낸다. fixture `leaf-type-widened` · `unknown-git-source` |
| F2 — 색인 대상 id 중복이 파일 순서로 조용히 해소된다 | HIGH | ACCEPT_NOW | `put()` 이 먼저 온 것을 남기고 중복을 `OPS_RC_ID_DUPLICATE` 로 이름 대며 낸다. fixture `target-id-duplicate` |
| F3 — 버전·매핑 위반이 더 앞선 `VALUE_UNSAFE` 를 가린다 | MEDIUM | ACCEPT_NOW | 해시 가능성 탐침을 버전 검사 **앞으로** 옮겼다. fixture `value-unsafe-with-future-version`(복합 예외 3번째) |
| F4 — 구버전 투영은 현행이 새로 보호하는 필드의 변화를 못 본다 | HIGH | ACCEPT_NOW | 원리적으로 관측 불가(과거 값이 원장에 없다). `migration_unattributable` 정수로 방출하고 `F-OPS-30-L6` 으로 한계를 명시했다. 판정은 여전히 fail-closed(구버전 행은 봉인 상태가 아니다) |
| F5 — 추가 전용 버전 매핑 검사가 순환이다 | MEDIUM | ACCEPT_NOW | `PINNED_VERSION_DIGESTS` 리터럴을 두고 `--selftest` 이 계산값과 대조한다(`pinned_version_digests 2`). 과거 투영을 고치면 그 상수를 손으로 바꿔야 한다 |
| F6 — 하네스가 인자에서 유도한 경로를 재귀 삭제한다 | HIGH | ACCEPT_NOW | `$SNAP` 에 소유 표식(`.ops30-snapdir`)을 심고 표식 없거나 `$SNAP` 바로 아래가 아닌 경로는 지우지 않는다(`rm_owned` / `sweep_temp`) |

- 라운드 수: **2** (R1 은 게이트 순서 때문에 빈 diff 를 봤다 · R2 가 실물 리뷰)
- Deferred to backlog: 0
- Open Questions: **없음** — R2 지적 6건 전건 흡수. F4 만 원리적 미검증이며 `F-OPS-30-L6` 에 사실로 기록했다
- 흡수 후 재실행: `--selftest` exit 0 · `bash ops/reconcile/_harness/m0.sh validate` → `validate ok` exit 0
  (fixture 31 case, 그중 R2 흡수를 인과로 무는 신규 4 case)

### Security Reviewer

> security-reviewer unavailable, skipped (auto-fallback): 이 세션은 Agent(Task) tool 호출이
> 금지돼 있어 `subagent_type: security-reviewer` 를 부를 수 없다.

대상 축은 **입력 검증**(원장 JSONL 폐쇄형 스키마 · 색인 레코드 선언 밖 키 거부)과
**경로 취급**(fixture tmp 트리 생성/복사)이다. 인증·비밀·암호 프리미티브·외부 요청은
이 마일스톤 산출물에 없다. 해시는 무결성 지문 용도이며 비밀을 다루지 않는다.

receipt 는 `security_skipped=true` 로 봉인되고, 이는 downstream `/mccp:pr` 에서
non-approving 으로 작동한다 (fail-closed 유지).

### Design Review

> impeccable silent-skip (auto-fallback): `design_signal=false` · reason `no-signal`.

`impeccable-detect.js detect --mode implement` 가 렌더 표면 0건을 냈다. 이 마일스톤의
산출물은 node CLI 와 JSONL 원장이고 `.tsx/.css/.html` 도 `.claude/cache/*` 도 없다.
계획서 §Design Routing Guide 가 같은 사실을 기록으로 남겨 두었다.

---

# Implementation Report — ops-30-reconcile M0

> 보고서 자리가 `.claude/PRPs/reports/` 가 아니라 여기인 이유: 계획 §Files to Change 가 도구 경로
> 허용을 5종으로 열거했고 `PRPs/reports/**` 는 그 밖이라, 거기 쓰면 acceptance 하네스의 소유 검사가
> `OUT_OF_SCOPE` 로 깨진다. 실측으로 확인했고 `F-OPS-30-H3` 으로 이월했다.

## Summary

층 3 대조 원장의 **스키마와 해시 규칙**을 확정하고 selftest·acceptance 하네스로 결박했다.
`ops/reconcile/reconcile.mjs` 한 파일에 하위 명령 5종(`--check` · `--selftest` · `--list-tokens` ·
`--print-rule` · `--fixture-tree`)이 있고, `ops/reconcile/reconcile.jsonl` 은 0바이트로 동결했다.
M0 종료 시점의 `--check` 는 **의도적으로 빨갛다** — `OPS_RC_LEDGER_EMPTY` 다.

## Assessment vs Reality

| Metric | Predicted (Plan) | Actual |
|---|---|---|
| Complexity | Medium | Medium |
| Files Changed | 5 (CREATE) | 11 CREATE (golden 트리가 데이터 파일 6개 + README 로 펼쳐짐) · 3 UPDATE (도구 경로) |
| 회부 | 9 | 9 (+ 범위 밖 3건 이월) |
| Codex 라운드 | cap 1 | 2 (R1 은 빈 diff · R2 가 실물 6지적, 전건 흡수) |

## Tasks Completed

| # | Task | Status | Notes |
|---|---|---|---|
| 1 | 하네스를 놓고 기준선을 뜬다 | 완료 | `preflight ok`. golden statement 2행이 ops-20 검사기 사본을 통과 |
| 2 | ops-40 canonical import + 토큰 경계 변환 | 완료 | `canonicalize_shared_reference 1` — 복제본이면 실패 |
| 3 | 코퍼스 공백 타입 적대 벡터 | 완료 | 벡터 16종. `corpus_type_gaps array,boolean,float,negative` |
| 4 | 코퍼스로 디스크 값 최신성 결박 | 완료 | `corpus_rows_checked 647` 전건 일치 |
| 5 | 원장 레코드 스키마 순수 함수 | 완료 | `schema_cases 8` |
| 6 | 대상 해석 + 선언 밖 키 거부 | 완료 | 실 색인 2919행 `index_unknown_keys 0` |
| 7 | `--check` 유효 verdict + 실패 | 완료 | 계수 25종. 토큰 한 번에 하나, 전표 순서 |
| 8 | golden 트리 + 결함 fixture | 완료 | 양성 5 · 음성 26 (토큰 19종 전수 대조). 음성 4건은 R2 흡수를 인과로 문다 |
| 9 | 0바이트 원장 + findings | 완료 | 마커 15개(회부 9 + 한계 6), 회부 9건 양쪽 등재 |

## Validation Results

| Level | Status | Notes |
|---|---|---|
| Static Analysis | 통과 | `node --check ops/reconcile/reconcile.mjs` · `bash -n ops/reconcile/_harness/m0.sh` |
| Unit / selftest | 통과 | `node ops/reconcile/reconcile.mjs --selftest` exit 0 |
| Acceptance 하네스 | 통과 | `bash ops/reconcile/_harness/m0.sh validate "$SNAP"` → `validate ok` exit 0 |
| Integration | 해당 없음 | 서버·빌드 산출물이 없다 |
| Edge Cases | 통과 | fixture **31 case** 전건이 기대 rc·토큰·계수 (양성 5 · 음성 26) |

Design Grounding: N/A (design trigger 미발화 — `impeccable-detect` `design_signal=false`, reason `no-signal`).
Phase 3.6 / 3.7 은 capture 산출물 부재로 완전 no-op.

### 실측으로 확인한 계수

```
corpus_rows_checked 647 · corpus_string 4401 · corpus_integer 1333 · corpus_null 581
corpus_nonascii_string 229 · corpus_type_gaps array,boolean,float,negative
index_git_rows 2870 · index_runtime_candidates 49 · index_unknown_keys 0
shape_comment 1370 · shape_commit 557 · shape_deleted_path 833 · shape_prod_only 102 · shape_revert 8
semantic_mutations_moved 6 · provenance_mutations_moved 0 · nested_unknown_key_caught 2
```

golden 트리 `--check`: `records_total 5` · `effective_agree/divergent/stale/unresolvable` 각 1 ·
`drift_eligible 4` · `stale_by_content_drift 1` · `content_drift_ratio 0.25` · `verdict ok`.

비율 fixture 실측 — 경계 `3/10 = 0.30` 통과, `4/10` 이 `OPS_RC_STALE_RATIO`.
`unresolvable` 90행을 더해도 `drift_eligible 10` 불변. runtime 메타 1행 실재 상태에서도 `0.30`.
`mixed-projection-version` 은 `stale_by_projection_migration 5` + `stale_by_content_drift 1` —
겹친 행이 두 계수에 **둘 다** 들어간다.

## Files Changed

| File | Action |
|---|---|
| `ops/reconcile/reconcile.mjs` | CREATED |
| `ops/reconcile/reconcile.jsonl` | CREATED (0 bytes, 명시적 생성) |
| `ops/reconcile/_harness/m0.sh` | CREATED |
| `ops/reconcile/_harness/golden/README.md` | CREATED |
| `ops/reconcile/_harness/golden/ops/_meta/sources.lock` | CREATED |
| `ops/reconcile/_harness/golden/ops/cells/s1-simwon-items.jsonl` | CREATED |
| `ops/reconcile/_harness/golden/ops/statements/statements.jsonl` | CREATED |
| `ops/reconcile/_harness/golden/ops/db/rows.jsonl` | CREATED |
| `ops/reconcile/_harness/golden/ops/reconcile/reconcile.jsonl` | CREATED |
| `ops/reconcile/_harness/golden/docs/_meta/index/policy-candidates.git.jsonl` | CREATED |
| `ops/reconcile/_harness/golden/docs/_meta/index/policy-candidates.runtime.jsonl` | CREATED |
| `ops/findings/ops-30-reconcile.md` | CREATED |
| `.claude/plans/codex-findings-backlog.md` | UPDATED |
| `.claude/state/fix-task.md` | UPDATED |
| `.claude/notes/ops-30-reconcile-m0.md` | UPDATED |

## Deviations from Plan

1. **투영 버전이 1개가 아니라 2개다.** 계획은 `projection_version` 을 두라고만 했는데, 마이그레이션
   귀속(D3 §1~4)과 그 fixture 4종(`mixed-projection-version` · `migration-backlog-over-50` ·
   `projection-version-future` · `projection-mapping-drift`)은 매핑에 항목이 **둘 이상** 있어야
   실행으로 증명된다. v1 은 반증된 R1 초안 투영(= `legal_boilerplate` 와 `anchor_commit` 를 출처로
   오분류한 판)으로 두고 v2 를 현행으로 했다. 지어낸 버전이 아니라 이 계획서가 실제로 반증한 형태다.
2. **runtime 축 중첩 선언을 더했다.** 계획 D3 은 중첩 키를 git 축에서만 열거했다. 실측하면 runtime
   후보에도 `source = {index, key}` · `reachability = {deployment_evidence, grade}` 두 중첩이 있고,
   "선언은 중첩 끝까지 내려간다" 는 같은 절의 규칙이 그 둘에도 걸린다. 안 적으면 재귀 미지 키 거부가
   그 subtree 에서 조용히 통과한다.
3. **원장 구조 토큰 2종의 담당 범위.** 전표에 원장 JSON 파싱 실패용 토큰이 없어
   `OPS_RC_RECORD_SCHEMA` 로, 말미 LF 부재는 `OPS_RC_LEDGER_NOT_LF` 로 흡수했다. 토큰 19종을
   늘리지 않았다.
4. **`env -u` 를 하네스에서 뺐다.** 계획은 미러(ops-20 하네스)를 그대로 따르라고 했는데 그 형태가
   이 호스트에서 node 를 조용히 삼킨다(`F-OPS-20-H1`). 셸 `unset` + 트리 cd + 상대 경로 + 비공허
   단언으로 바꿨다.
5. **PRD 마일스톤 상태를 넘기지 않았다.** `F-OPS-30-H2` — 계획의 허용 5종에 `.claude/prds/**` 가
   없어 플립이 acceptance 를 깬다(반사실 실측 rc 1). `in-progress` 로 남겨 뒀다.
6. **보고서 위치.** `F-OPS-30-H3` — 위와 같은 뿌리. 이 파일 안에 실었다.
7. **R2 흡수로 계획에 없던 것 셋이 늘었다.** `OPEN(사유)` 스펙 노드(열린 subtree 를 이름으로
   연다) · `PINNED_VERSION_DIGESTS` 리터럴(추가 전용을 기계로 문다) · 하네스의 snapdir 소유
   표식(재귀 삭제 범위 제한). 셋 다 계획의 규칙(선언 밖 키 거부 · 매핑 추가 전용 · 임시 트리
   sweep)을 **약속에서 기계로** 옮긴 것이고 새 정책을 만들지 않았다.

## Issues Encountered

- **`env -u ... node <절대경로>` 무음 실패** (MSYS bash). rc 0 · stdout 0바이트. 반사실로 원인 확정
  (같은 인자를 `env -u` 없이 부르면 정상). ops-20 하네스가 이 형태를 쓴다 → `F-OPS-20-H1` 이월.
- **`sed -i` 가 줄 끝을 LF 로 정규화한다.** PRD 를 편집했다가 되돌렸을 때 내용은 같은데 바이트가
  달라 소유 검사가 계속 빨갰다. CRLF 로 복원해 baseline 해시(`af6c866556a523d4`)와 바이트 일치를
  확인한 뒤 `validate ok` 가 재현됐다.

## Next Steps

- [ ] `/mccp:pr` — 다만 이 receipt 는 `codex_verdict=divergent` + `security_skipped=true` 로 non-approving 이다
- [ ] Implement-Codex R2 — 산출물 경로를 명시해 재실행 (아래 절)

## 로컬 리뷰 (`/mccp:code-review`, 무인자 — Local Review Mode)

Codex 가 아니라 이 세션의 로컬 리뷰다. receipt 를 쓰지 않는다(advisory 모드). 지적 9건
(HIGH 3 · MEDIUM 3 · LOW 3) **전건 수용**. 아래는 흡수 내용과 각 건의 반사실이다.

| # | 지적 | 흡수 |
|---|---|---|
| HIGH-1 | `ops/reconcile/**/*.jsonl` 이 `.gitattributes` 밖 · `core.autocrlf=true` | `.gitattributes` 에 `ops/reconcile/**/*.jsonl text eol=lf` |
| HIGH-2 | `.claude/state/fix-task.md` 부재로 11) 이 실패 | `$FIXTASK` 가 `fix-task-applied.md` 로 폴백 |
| HIGH-3 | 더러운 `.claude/settings.json` · `.claude/prds/*.prd.md` 가 허용 열거 밖 | 열거에 3경로 추가 + 열거를 **한 곳으로** 통합 |
| MEDIUM-1 | `--selftest` 이 선언하지 않은 토큰을 낸다 | `SELFTEST_TOKENS` 를 도달 가능 6건으로. 하네스가 크기·공유분을 문다 |
| MEDIUM-2 | 색인 파손이 원장 토큰 `OPS_RC_RECORD_SCHEMA` 로 나온다 | 신설 `OPS_RC_INPUT_NOT_JSONL` + fixture `index-not-jsonl` |
| MEDIUM-3 | `rm_owned` 의 셸 glob 이 `..` 탈출을 통과시킨다 | `under_snap()` — node `path.resolve` 접두 대조 |
| LOW-1 | 색인 행/파일의 조용한 탈락 | 계수 2종 방출 + `TARGET_ABSENT` 진단문에 사유 병기 + `readJsonlFile` 은 ENOENT 만 삼킨다 |
| LOW-2 | 중복 키 구분자 (**지적이 틀렸다** — 아래) | NUL → `JSON.stringify` |
| LOW-3 | 실 코퍼스 계수 결박이 하네스를 일회용으로 만든다 | 헤더에 명시 |

### 반사실

- **HIGH-1** — scratch repo(`autocrlf=true`)에 LF 2행 커밋 → clone → checkout: `od -c` 가
  `} \r \n` 을 냈다. 같은 규칙을 `.gitattributes` 에 넣고 반복하면 `\n` 을 유지한다. 저장소
  방증: 속성 밖 `ops/db/rows.jsonl` 은 CR 669바이트, 속성 안 `docs/_meta/index/*.jsonl` 은 0.
  영향 반경은 (가) `_harness/golden/**` 교차 오라클이 신선 체크아웃마다 `GOLDEN_CHECK_NONZERO`
  (나) M2 가 행을 넣는 순간 생산 `--check` 가 항상 `OPS_RC_LEDGER_NOT_LF`.
- **HIGH-2** — `.claude/state/fix-task.md` 는 stop-review-loop 가 소진 후 `fix-task-applied.md`
  로 개명한다. 개명본에 `F-OPS-30-R1..R9` 9건 전건 실재(실측 9/9). 앞 판 notes 의 `validate ok`
  는 개명 이전 상태에 대한 것이라 그대로는 재현되지 않았다.
- **HIGH-3** — 9) 판정기를 원문 그대로 떼어 합성 경로 9건으로 물렸다. 새 3경로는 통과,
  `.claude/prds/ops-20-statements.prd.md`(다른 PRD) · `ops/db/rows.jsonl` · `README.md` 는 거부.
  `.claude/**` 통째 면제가 아니라 **열거 확장**임이 그 대조로 확인된다.
- **MEDIUM-1/2** — 색인 1행을 깨면 앞 판은 `--check`·`--selftest` 둘 다
  `OPS_RC_RECORD_SCHEMA`(원장 행 토큰)를 냈다. 지금은 `OPS_RC_INPUT_NOT_JSONL` 이고 두 모드
  `--list-tokens` 에 다 실린다.
- **MEDIUM-3** — 같은 입력 `$SNAP/../../victim` 에 대해 옛 glob 은 `old-ALLOW`, `under_snap` 은
  `REJECT`. `$SNAP` 자기 자신도 거부한다.
- **LOW-1** — 색인 파일을 지우면 앞 판은 `OPS_RC_TARGET_ABSENT` 만 냈다. 지금은 같은 줄에
  `· 대상 색인 파일 부재 1건: docs/_meta/index/policy-candidates.git.jsonl` 이 붙고, 좌표
  오탈자 대조군에는 안 붙는다. 실패 경로는 stdout 이 비므로(부분 리포트 금지) 계수 방출만으로는
  이 구분이 안 보였다 — `OPS_RC_LEDGER_EMPTY` 가 `ledger_bytes` 를 stderr 에 싣는 것과 같은 자리다.

### 지적이 틀렸던 것 (LOW-2)

원 지적은 *"중복 키가 공백 결합이라 `("a b","c")` 와 `("a","b c")` 가 충돌한다"* 였다. **틀렸다.**
구분자는 공백이 아니라 NUL(0x00)이었고 stmt id 에 NUL 은 나올 수 없으므로 모호하지 않았다.
Read 렌더링이 NUL 을 공백으로 보여 준 것을 그대로 읽은 오독이다. 그래도 교체는 남긴다 —
사유가 다르다: 소스에 든 0x00 이 `grep` 에 이 파일을 binary 로 만들어 검색·패치가 막힌다(실측:
`grep -n pairSeen` 이 `Binary file ... matches` 를 냈고 문자열 편집이 실패했다).

### 토큰 집합이 계획서 전표와 갈렸다

계획 §토큰 우선순위 전표는 19개다. 지금 구현은 **20개**(`OPS_RC_INPUT_NOT_JSONL` 이 4번,
그 뒤가 한 칸씩 밀림). 계획 본문은 `mccp-plan-codex` receipt 의 `plan_hash` 앵커라 고치지
않는다 — 갈린 사실을 여기에 적는 것이 정본이다. 우선순위 **관계**는 보존됐다(신설 토큰이
구조 그룹 끝, 행 그룹 앞).

### 재검증

기준선을 새로 잡고 하네스를 통째로 다시 돌렸다(산출물 3종을 저장소 밖으로 옮겨 `preflight`,
되돌린 뒤 `validate`).

```
preflight ok
tool_paths_changed 0
validate ok        rc=0
```

fixture 는 31 → **32** case(신규 `index-not-jsonl`), 토큰 전수 대조는 20/20.
