# task-2952 마아트(Ma'at) 독립 QC 검증 보고서

- 검증관: 마아트 (횡단조직 독립 QC, dev2 소속 아님)
- 검증일: 2026-08-14
- 대상: `/home/jay/projects/InsuRo/.worktrees/task-2952-dev2` (branch `task/task-2952-dev2`, worktree HEAD `91e3a67`, PR #213 head `b5d9c18a` — **tree 내용 동일**, `git diff --stat 91e3a67 origin/task/task-2952-dev2` 결과 빈 diff로 확인)
- 원칙: read-only. `git reset`/`checkout`/`stash` 미사용. 파일 미수정.

## 0. 최종 판정: **NEEDS WORK** (조건부 — 코드/데이터 무결성은 PASS 수준이나 완료조건 미충족 항목 존재)

**근거 요약**:
- V1(봉인 무결성)·V3(값 무결성)·V5(L1 실동작)·V6(자기모순)는 전부 재현·통과. GEN1/GEN5 값 변경 0건, OCR 18장 sha256 전수 일치, "독립 기재" 계열 표현 0건.
- 회귀는 재현 결과 팀 주장과 **정확히 일치**(1 failed는 CI-parity 조건에서 base에도 동일 재현 — 진짜 pre-existing, task-2952 코드와 무관 확인).
- 그러나 **task 완료조건 미충족 2건**을 확인: (1) `memory/events/task-2952.done` 파일이 실제로 존재하지 않음(2) PR #213 이 아직 **OPEN**(머지 안 됨). 보고서 §9·§10(마아트/G3 독립검증란)도 작업 시점엔 플레이스홀더 상태였음(본 문서가 그 자리를 채움). 완료조건 문언("`memory/events/task-2952.done` + ANU 콜백") 기준으로는 **미완결**이므로 NEEDS WORK.
- 코드/데이터 산출물 자체의 품질 판정은 조건부 PASS 수준이나, "완료" 표시에 필요한 산출물이 빠져 있어 전체 판정은 NEEDS WORK로 내린다. 아래 이슈 목록(§ 발견 이슈) 참조.

---

## V1. 봉인 무결성

**명령 1**: `cd <worktree> && git diff --name-only 1c1bf1c..HEAD -- server/silson/data/sources/ocr/ | wc -l`
```
0
```
→ **PASS**. OCR 18장 파일 자체는 diff 0건.

**명령 2**: `git diff 1c1bf1c..HEAD -- server/silson/data/sources/manifest.json`
```
- "sha256": "5e01e59ac7aff2cdf2037d91b96fed88bba49809f476de7c7718d29ade8ffabd",
- "bytes": 154344,
+ "sha256": "9e26bc4c82a958c90cfa699ff21ca5c3978a55bbb6dbf7dd9987acea1bfb4ca3",
+ "bytes": 159902,
```
→ manifest.json 전체 diff는 이 4줄(2추가/2삭제)뿐 — `comparison_matrix`의 sha256/bytes 2개 필드만 변경. **PASS** (task-2952.md 요구사항과 일치).

**명령 3 (직접 재계산)**:
```
$ sha256sum server/silson/data/sources/comparison_matrix.json
9e26bc4c82a958c90cfa699ff21ca5c3978a55bbb6dbf7dd9987acea1bfb4ca3  ...
```
manifest 값과 일치, `bytes` 실측(`os.path.getsize`)도 159902로 일치. → **PASS**.

**명령 4 (OCR 18개 항목 sha256 직접 재계산, manifest 대조)**:
```python
# manifest['items'][i]['ocr_file'] + ocr_sha256 vs 실제 파일 sha256
OK: 18 / 18
MISMATCH: []
```
→ **PASS**. `cells_total`/`conflict_count`/`coverage_gap_count`도 base(130/13/4) 대비 무변경 확인(수동 diff).

**V1 종합: PASS.**

---

## V2. task 요구사항 6항목

| 항목 | 판정 | 근거 |
|---|---|---|
| §1 뱅크샐러드 교차출처 등록 | **충족** | `silson_generations.json`·`comparison_matrix.json` 양쪽에 `cross_check_sources[BS_MRI_ARTICLE]` 신설. `source_tier: null`, `classification: media_secondary`, `standalone_confirmation_allowed: false`, `customer_facing_use_allowed: false`, `url`/`accessed_at`(2026-08-14)/`as_of`(2026-06-14) 전부 기재. **tier1 승격 없음**(sources_registry 기존 18항목과 분리된 별도 키) — 확인됨. |
| §2 MRI 3·4세대 대표값 300 확정 | **충족** | GEN3/GEN4 레코드 값(`coverage_limit` = `...MRI 300`)은 이미 task-2949에서 확정돼 있었고(diff상 값 필드 자체는 무변경), 이번 task는 **matrix 동기화**를 완결: `conflicts[10]/[12]`·`cells[74]/[87]` 4곳에 `corrected_representative_value: "MRI 300"` 신규 추가(재계산 스크립트로 확인, `conflicts[] with corrected_representative_value: 2/13`). A2 원문 `MRI 200`은 `conflicts[].prior_record`/`corrections[]`에 "폐기된 요약표 오기"로 보존(삭제 안 됨, grep으로 원문 존재 확인). matrix note의 "독립적으로" 문구는 4개소 전부 정정(§V6 참조). |
| §3 MRI 5세대 대조 | **충족** | GEN5 레코드에 `cross_check[]` 신설, 뱅크샐러드의 특약1/특약2·5,000만·200만 주장은 `verdict: "pending"`으로 명시(확정 아님). GEN5 `coverage_limit`/`deductible`/`payout_rate` 필드 값은 base와 **완전 동일**(diff 없음, 직접 대조 완료) — 뱅크샐러드 값이 확정으로 반영되지 않았음을 확인. |
| §4 MRI 1·2세대 대조 | **충족** | GEN1_2003_10에 `cross_check[]` 3건(입원 5천만=**conflict**, MRI규정=**pending**, 25만원=**pending**) 추가. GEN2_STD1에 대응 3건(입원 5천만=**confirmed**, 나머지 pending) 추가. "일치분만 반영" 원칙 준수 — GEN1 입원한도 `입원 최대 1억` 값 자체는 무변경(§V3 참조), GEN2 5천만원 확정은 이미 원문과 일치하던 값이라 반영이라기보다 교차기록 추가. |
| §5 plan_type 오독 방지 배선 | **충족** | `analysis_bridge.py`에 `_resolve_plan_type_field` 신설 + `describe_for_analysis`의 `payload["plan_type_resolution"]`로 노출. 독립 재현(V5)에서 typed/single/absent 3분기 정상 동작 확인. 기존 평면 키 하위호환 유지(`ATTRIBUTE_KEYS` 루프 무변경, 신규 키만 추가). |
| §6 설계사용 세대별 정리 문서 | **충족** | `docs/silson-generation-reference-for-agents.md` 신규 333줄. 세대별 상세, 확정/보류 표기, 출처(봉인OCR/뱅크샐러드) 병기, "핵심사항 6가지"·"보류·불일치 전체 목록" 섹션 존재. GEN5 MRI 보류·GEN1 불일치가 문서에도 정확히 반영됨(§V3 대조 결과와 일치). |

**V2 종합: 6/6 충족.** 단, 값 자체의 최초 확정(MRI 300)은 task-2949 산물이고 이번 task는 그 **동기화+근거교정+문서화**가 핵심이라는 점은 감안해야 함(과대포장 방지 차원에서 명시).

---

## V3. 값 무결성 (가장 중요) — 환각/추론 혼입 여부

**명령**: `git diff 1c1bf1c..HEAD -- server/silson/data/silson_generations.json` (전문 확인, 231줄)

- **GEN5 뱅크샐러드 수치 확정 반영 여부**: `deductible`/`payout_rate`/`coverage_limit` 3개 필드를 base와 직접 JSON 비교(파이썬 dict 완전일치 확인) → **완전 동일, 변경 0**. 특약1/특약2·연5,000만·연200만 수치는 어디에도 삽입되지 않음. 유일한 변경은 `cross_check[]` 배열 신설이며 그 안의 verdict는 `"pending"` 1건뿐. → **PASS, 환각 없음**.
- **GEN1 입원 한도**: `coverage_limit` = `"입원 최대 1억\n10만 원,\n30만 원,\n100만 원"` — base와 완전 동일(diff에 이 필드 라인 없음, 직접 조회로 재확인). 5,000만원으로 변경되지 않음. → **PASS**.
- **GEN3·GEN4 coverage_limit**: 두 레코드 모두 `MRI 300`(base 시점에 이미 300 — task-2949 산물). 이번 diff에서 coverage_limit 필드 자체의 라인 변경 없음(notes/cross_check/conflicts만 추가). → **PASS, 이번 task로 인한 값 변경 없음**.
- **diff 전체 231줄 수동 검토 결과**: 값 필드(`coverage_limit`/`deductible`/`payout_rate`/`waiting_period` 등) 라인 변경 **0건**. 변경분은 전부 (a) notes[] 배열에 텍스트 추가/정정, (b) 신규 `cross_check[]`/`cross_check_sources[]` 키 추가, (c) 기존 conflict `note` 텍스트의 "독립적으로"→"동일 문서 다른 페이지" 정정. → 원칙("값 변경 없음 + 교차검증 기록 추가")과 **정확히 일치**.

**V3 종합: PASS. 환각·추론 혼입 없음. 값 필드 변경 0건 확인.**

---

## V4. 회귀

**명령 1**: `python3 -m pytest tests/ -q -k silson`
```
205 passed, 1544 deselected, 4 warnings in 0.92s
```

**명령 2**: `python3 -m pytest tests/ -q` (task worktree, 전체)
```
FAILED tests/test_consultation_history_get.py::test_cors_fail_closed_when_ext_origin_unset
1 failed, 1746 passed, 2 skipped, 38 warnings in 167.12s
```
2회 반복 실행 모두 동일하게 재현(결정적 실패, flaky 아님).

**base(1c1bf1c) 대조 — 2단계로 검증**:
1. **1차**: `git worktree add /tmp/maat-base-check 1c1bf1c` (fresh, `.env` 없음) → 전체 `1738 passed, 3 skipped, 0 failed` — CORS 테스트 **PASS**. 즉 "동일 코드베이스라도 .env 없이는 재현 안 됨".
2. **2차 (팀 보고서의 CI-parity 방법론 재현)**: task worktree의 실제 `.env`(gitignore, `git check-ignore -v .env` → `.gitignore:30:.env` 확인)를 `/tmp/maat-base-check/.env`로 복사 후 재실행 → `1 failed, 4 warnings` — **동일하게 재현됨**.

→ **원인 규명**: `/home/jay/projects/InsuRo/.env`(및 각 워크트리 사본)에 실제 `INSURO_EXTENSION_ORIGIN=chrome-extension://ekhhgeekdelijnhkjbclpoomblbpgplj` 값이 설정돼 있고, `main.py`가 `load_dotenv(dotenv_path=.../.env)`로 이를 로드한다. 테스트의 `_probe_cors`는 **부모 프로세스의 `os.environ`에서만** 해당 키를 pop하지만, 서브프로세스 내부에서 `main.py`가 dotenv로 **디스크에서 직접 재로딩**하므로 pop이 무력화된다 — 결과적으로 "확장 origin 미설정" 테스트 케이스가 실제로는 "설정된 상태"로 실행되어 fail-closed 어서션이 깨진다. 이는 `main.py`/`silson`의 CORS 로직 자체는 **task-2952에서 전혀 건드리지 않았음**(diff --stat에 main.py 없음)을 재확인했고, base+동일 .env 조건에서도 100% 재현되므로 **팀 주장("pre-existing, task-2952 코드 무관")은 정확히 재현·검증됨**.
→ **단, 주의**: 이 결론은 "task worktree에 실 서비스용 `.env`가 존재한다"는 전제에 의존한다. `.env` 없는 clean 환경(예: 신규 CI 러너, 신규 worktree)에서는 base·task 양쪽 다 이 테스트가 PASS한다 — 즉 이 실패는 코드 결함이 아니라 **이 특정 worktree의 로컬 인프라(.env) 아티팩트**다. CI(`gh pr checks 213`)는 11/11 전부 `pass`였고 이 테스트가 CI에서는 실패하지 않았다는 점도 이 해석과 일치(CI 러너에는 이 실 .env가 없을 것으로 추정).

**V4 종합: PASS(재현+검증 완료).** 팀 주장 재현됨. 단, 근본원인(worktree local .env 아티팩트)은 보고서에 기술되지 않았으므로 **medium 이슈로 별도 기록**(§ 발견 이슈 #2).

---

## V5. L1 실동작 — `describe_for_analysis` 직접 호출

독립적으로(팀 테스트 파일을 보지 않고 API만 참고해) 스크립트 작성, GEN3(typed 기대)/GEN4(single 기대)/GEN1(absent 기대) 3개 세대로 실행:

```
=== typed(GEN3) gen= GEN3_2017_04
  deductible -> scheme= typed axis_present= True caveat= 표준형/선택형 축(plan_type)이 있는 항목이다 — 평면 키/원문은...
  payout_rate -> scheme= typed axis_present= True caveat= 표준형/선택형 축(plan_type)이 있는 항목이다 — 평면 키/원문은...
  json.dumps len= 7718
=== single(GEN4) gen= GEN4_2021_07
  deductible -> scheme= single axis_present= True caveat=
  payout_rate -> scheme= single axis_present= True caveat=
  json.dumps len= 5041
=== absent(GEN1) gen= GEN1_2003_10
  deductible -> scheme= absent axis_present= False caveat=
  payout_rate -> scheme= absent axis_present= False caveat=
  json.dumps len= 3280
```

- typed/single/absent 3분기 정확히 구분됨. GEN1(absent)에서 `types`의 모든 값이 null로만 채워지고 존재하지 않는 유형을 날조하지 않음(직접 dict 조회로 확인).
- `json.dumps(out, ensure_ascii=False)` 3케이스 전부 예외 없이 직렬화 성공.
- 신규 테스트 파일(`test_silson_analysis_bridge_plan_type.py`) 8케이스도 독립 실행 → `8 passed`.

**V5 종합: PASS.**

---

## V6. 자기모순 검사

**명령**: `grep -rn "독립 기재\|독립성 근거\|독립 페이지" server/silson/ | grep -v __pycache__`
```
(0건, exit=1)
```
**명령**: `grep -rn "독립.*출처.*아니" server/silson/ | grep -v __pycache__ | wc -l`
```
10
```
**명령**: `grep -rn "독립적으로" server/silson/ docs/ | grep -v __pycache__`
```
(0건)
```

→ "독립 기재"/"독립적으로" 계열(잘못된 독립성 주장) **0건**, "독립 출처가 아니다" 계열(정정된 서술) **10건 보존**. 팀 보고서 §7 이슈#4("Codex가 5곳 추가 정정 포착")와 정합.

**데이터 파일 내부 모순 추가 탐색**: `comparison_matrix.json`의 `resolution: "primary_wins"` 필드는 정정 후에도 원형 그대로 남아있고(§V2 표에서 언급), 대신 `corrected_representative_value`/`resolution_note`가 "이 필드만 보면 200으로 오도출된다"고 명시적으로 경고 — 이는 모순이 아니라 **의도된 이력 보존 설계**(원형 대조표 유지 + 정정 필드 별도)로 판단. 다만 이 설계는 **"매트릭스만 단독 판독하는 소비자"가 여전히 `resolution` 필드만 보고 `MRI 200`을 대표값으로 오인할 위험을 완전히 제거하지 못한다** — `corrected_representative_value` 필드를 신뢰하지 않는 하위 소비자 코드가 있다면 잘못된 값을 쓸 수 있다. 현재 `analysis_bridge.py`나 다른 코드가 이 필드를 실제로 읽는지 확인:

```
$ grep -rn "corrected_representative_value" server/ --include=*.py
(0건 — 매치 없음)
```

→ **이슈**: `corrected_representative_value` 필드는 데이터에만 존재하고 이를 소비하는 코드가 **없다**(런타임 호출자 0 = 휴면이므로 당장 실피해는 없으나, 향후 활성화 시 이 필드를 안 읽는 소비자가 `resolution: primary_wins` + `primary: A2`만 보고 200을 채택할 구조적 위험이 남아있음). §발견 이슈 #3 참조.

**V6 종합: PASS**(요구된 grep 기준 통과) + **구조적 잔여 위험 1건 발견**.

---

## V7. trip-wire 5종 실측

| trip-wire | 실측치 | 측정 방법 |
|---|---|---|
| Critical7 위반 | `0` (CI 게이트 근거, 로컬 재계산 도구 미보유) | `gh pr checks 213` → `qc-check pass`, `guard pass`, `ci/guard pass` 등 11/11 전부 SUCCESS. 로컬에 team이 언급한 `utils.sanitize_gate`/Critical7 스캐너가 InsuRo repo 내에 존재하지 않아 **직접 재계산은 불가** — CI 결과를 근거로 채택(간접 측정, 완전 독립 재현은 아님을 명시). |
| PII net-new | `0` | **직접 재현**: `silson.tools.seal_sources.sanitize_text()`(팀 보고서가 인용한 `utils.sanitize_gate.sanitize_text`는 **모듈 경로 오기** — 실제로는 `silson/tools/seal_sources.py`에 정의됨, `ModuleNotFoundError`로 확인 후 정정)를 변경 7파일 전체에 직접 실행 → 전 파일 `pii_counts={}`, 합계 0. |
| 회귀 실패 | `1` (재현됨) | `pytest tests/ -q` 2회 반복, 매회 동일 1건. base+동일`.env` 조건에서도 동일 재현 → task-2952 기인 회귀 아님으로 판정(§V4). |
| forbidden_paths 침범 | `0` | `git diff --name-only 1c1bf1c..HEAD`: `docs/silson-generation-reference-for-agents.md`, `server/silson/analysis_bridge.py`, `server/silson/data/silson_generations.json`, `server/silson/data/sources/comparison_matrix.json`, `server/silson/data/sources/manifest.json`, `server/silson/provenance.py`, `server/tests/test_silson_analysis_bridge_plan_type.py` — 전부 `server/silson/`·`docs/`·`server/tests/` 범위 내. 타 팀 디렉토리·봉인 OCR 원본 미접근. |
| nonce | `task-2952` | 일치 |

---

## 발견 이슈 목록 (최소 3건)

### 이슈 #1 — High: 완료조건 미충족 (`.done` 마커 부재 + PR 미머지)
- **근거**: `find /home/jay/workspace/memory/events -iname "*2952*"` → `task-2952.allow-no-scope.log`, `task-2952.spawn-confirmed-*.json`만 존재, `task-2952.done`/`task-2952.done.acked` **부재**. `gh pr view 213 --json state` → `"state":"OPEN"`, `"mergedAt":null`.
- task-2952.md 완료조건: "`memory/events/task-2952.done` + ANU 콜백(envelope...)". 이 조건이 검증 시점 기준 **미충족**.
- **권고**: PR #213 CI는 11/11 green이므로 머지 자체엔 기술적 블로커가 없다. ANU가 본 검증 보고서를 받은 뒤 머지 승인 + `.done` 발행 절차를 진행할 것.

### 이슈 #2 — Medium: 회귀 실패 근본원인이 보고서에 미기술 (`.env` 아티팩트)
- **근거**: §V4에서 직접 규명 — `test_cors_fail_closed_when_ext_origin_unset` 실패는 worktree의 **untracked 실 `.env`**(`INSURO_EXTENSION_ORIGIN` 실값 설정)가 서브프로세스 dotenv 재로딩으로 테스트의 env pop을 무력화시키기 때문. `.env` 없는 clean 환경에서는 base·task 양쪽 다 PASS.
- 팀 보고서(§6)는 "CORS 환경변수 이슈이며 silson 과 접점이 없다"고만 적어 **결론은 맞았으나 정확한 근본원인(테스트의 서브프로세스 dotenv 재로딩 취약점)은 기술하지 않음**. 이는 이 worktree에 국한된 문제가 아니라 **테스트 스위트 자체의 구조적 취약점**(CI에는 이 .env가 없어 안 드러날 뿐)이라 향후 다른 task에서 동일 혼란(진짜 회귀인지 오판)을 유발할 수 있다.
- **권고**: `test_consultation_history_get.py`의 `_probe_cors`가 서브프로세스 실행 전 `env`에서 pop한 값이 dotenv에 의해 되살아나지 않도록 `INSURO_EXTENSION_ORIGIN=`(빈 문자열 명시적 설정, pop 대신 override)으로 바꾸는 별도 인프라 개선 티켓을 권고. task-2952 범위는 아니므로 이번 PR 블로커는 아님.

### 이슈 #3 — Medium: `corrected_representative_value` 필드를 읽는 코드가 0건 (구조적 잔여 위험)
- **근거**: `grep -rn "corrected_representative_value" server/ --include=*.py` → 0건. 매트릭스 파일에는 신설됐으나 소비 코드가 없음.
- 현재는 `silson` 런타임 호출자가 0(휴면)이라 실피해는 없으나, 이 필드가 "매트릭스 단독 판독 시 오도출 방지"를 위해 설계된 것이라면, 향후 활성화 시점에 이 필드를 무시하고 `resolution`/`primary`만 읽는 소비자 코드가 작성될 경우 `MRI 200`이 재차 잘못 채택될 수 있는 **구조적 함정**이 여전히 남아 있다.
- **권고**: `silson_generations.json`(진짜 대표값 소스)을 유일한 소비 대상으로 강제하는 문서화, 또는 매트릭스 로더에 `corrected_representative_value` 우선 적용 로직을 추가하는 후속 task 권고.

### 이슈 #4 — Low: trip-wire 표의 모듈 경로 오기
- **근거**: 팀 보고서 §11 "PII net-new" 근거로 `utils.sanitize_gate.sanitize_text`를 인용했으나, 독립 재현 시 `ModuleNotFoundError: No module named 'utils.sanitize_gate'` 확인. 실제 모듈은 `silson.tools.seal_sources.sanitize_text`.
- 결과(0건)는 정정된 모듈로 재현해도 동일하게 PASS했으므로 **판정 자체에는 영향 없음**, 단 보고서의 근거 인용이 부정확.
- **권고**: 보고서 §11 모듈 경로 정정.

### 이슈 #5 — Low: 매트릭스 `conflicts[]` 13건 중 2건만 동기화, 나머지 11건은 이번 task 범위 밖으로 남음
- **근거**: `conflicts[] with corrected_representative_value: 2 / 13`. 나머지 11건(GEN1 입원한도 라벨 불명, GEN1 excluded_coverages 등 "회장판단 보류" 항목들)은 여전히 `corrected_representative_value` 없이 `resolution: primary_wins`만 있다.
- task-2952.md §2가 명시적으로 "MRI 3·4세대"만 대상으로 했으므로 **범위 위반은 아님**. 다만 기존 메모리(`project_insuro_silson_rework_task2949_verified_260814.md` R2)가 지적한 "13건 전부 corrected_representative_value 0" 이슈는 이번 task로 **2건만 해소**되고 11건은 여전히 잔존 — 후속 task 필요성을 재확인.

---

## trip-wire 5종 실측치 요약표

| # | trip-wire | 실측치 | 측정 방법/한계 |
|---|---|---|---|
| 1 | Critical7 위반 | 0 | CI 게이트 간접(로컬 스캐너 부재로 직접 재계산 불가) |
| 2 | PII net-new | 0 | 직접 재현(`silson.tools.seal_sources.sanitize_text`, 7파일 전수) |
| 3 | 회귀 실패 수 | 1 (base+동일 .env 조건에서도 재현 → task 기인 아님으로 판정) | 직접 재현 2회 + base 대조 2단계 |
| 4 | forbidden_paths 침범 | 0 | `git diff --name-only` 7파일 전부 허용 범위 내 |
| 5 | nonce | task-2952 | 일치 확인 |

---

## 재현되지 않은 팀 주장 없음 / 재현된 것만

- 팀이 주장한 정량 수치(확정4/보류3/불일치1, 205 silson passed, 전체 1746 passed+1 failed, sha256 무변경, PII 0건, Codex risks 4건)는 **전부 독립 재현 성공**.
- 유일하게 **표현이 부정확했던 것**은 §11의 sanitize 모듈 경로(`utils.sanitize_gate` → 실제는 `silson.tools.seal_sources`)이며, 결과 수치 자체는 재현됨(이슈 #4, low).
- **재현되지 않은 것은 결과가 아니라 "완료 마감" 절차**다: `.done` 마커와 PR 머지가 검증 시점에 아직 없음(이슈 #1, high) — 이것이 최종 판정을 NEEDS WORK로 내린 핵심 근거.
