# task-2787+7 보고서 — 스케일 적용 + 선택기준 oracle + row-level 증거

- 팀: dev5-team (마르둑)
- 레벨: Lv.3 / 진단 관찰 필드 확장
- 브랜치: `task/task-2787+2-dev5` (same-branch 계속 커밋, **PR·merge·배포 없음**)
- 워크트리: `/home/jay/projects/InsuRo/.worktrees/task-2787+2-dev5`
- 작성일: 2026-07-18

---

## S — Situation
회장 self-smoke #2 실측으로 DOM `data-cd` == API `coverage_cd` 가 확정되었고, 가입금액 input 오채택 위험도 배제되었다. 그러나 스케일 적용 계산값(333,654)과 화면 표시 합계(343,414) 사이 약 2.8% 잔여차가 남아 있고, 집계 비교만으로는 원인을 특정할 수 없는 상태였다.

## C — Complication
- 잔여차의 원인 후보(스케일 적용 방식 / 화면 합계의 선택 필터 부재 / 특정 행의 이상값)를 **집계 수준에서는 구분 불가**.
- `em[premium]` 이 화면 스케일값인지 API 기준값인지 **UNKNOWN** 상태 — 단정하면 오진.
- 기존 계산 경로를 건드리면 지금까지의 실측 기준선이 무너진다.

## Q — Question
기존 필드·계산식을 **한 글자도 바꾸지 않고**, 같은 조회에서 row-level 증거와 선택기준 oracle을 병렬로 관찰할 수 있는가?

## A — Answer
가능하며, 구현·검증 완료했다. 모든 신규 값은 **병렬 필드**로만 추가했고 기존 필드는 실행 결과로 무변경임을 확인했다.

---

## 구현 내용 (F1~F5)

수정 파일: `extension/inject.js` **1개** (신규 파일 0)
테스트 파일: `extension/__tests__/dom_selection_snapshot.test.ts` **1개**

### F1. 스케일 합계
- `parseDomAmount(raw)` — `"10,000"` → `10000`. 비숫자 제거 후 파싱, 비양수·실패는 `null`(추측 금지).
- `computeScaledSelectionForCompany(...)` — `Σ api_premium × (dom_amount_parsed / coverage_amount)`.
- `scaled_included_count` = 실제 합산 기여 행수 (tolerance 재현용).
- 기여 행 0건이면 `dom_selected_scaled_sum = null` (0 날조 금지).
- **제외 사유 9종 taxonomy 고정 순서** (첫 매치 1건만 카운트, 조용한 통과 없음):
  `dom_duplicate` → `api_missing` → `api_duplicate` → `differs_across_companies` → `api_premium_missing_or_invalid` → `negative_invalid` → `zero_premium_unverified` → `zero_baseline` → `parse_fail`
- 9종 키는 **항상 전부 존재**(0 포함). 회사별 `scaled_excluded_reasons` + 전체 `scaled_excluded_reasons_total` 병기.

### F2. 선택 기준 oracle
- `scrapeScreenSelectedCells(rootEl)` — `#premium_lists em[company_code][coverage_cd][premium]` 를 `{회사}{담보}` 맵으로 수집.
- `screen_selected_total` = **선택 cd 소속 셀만** 합산.
- 기존 `screen_total`(전체합) **무변경 유지** — 대조용.

### F3. 스케일 분류
- `scaled_classification` / `scaled_diff` — 기존 `classifyScreenTotalMatch` 를 **수정 없이 재사용**, tolerance 기준만 `scaled_included_count`.
- 기존 `classification` 무변경.

### F4. row-level 증거
- `row_evidence[]` = **전 회사 × 선택담보(중복제거) 전량**. 샘플링·상한 없음.
- 정렬: `company_code` → `cd` 오름차순 고정.
- 13개 키: `company_code, cd, api_premium, api_base_amount, api_guide_premium, api_ratio, dom_amount_raw, dom_amount_parsed, scaled_value, screen_cell_premium, screen_cell_text, diff_scaled_vs_cell, excl_reason`
- 셀 미발견 시 `screen_cell_premium`/`screen_cell_text` 둘 다 `"UNKNOWN_NO_CELL"` — 0 대입·추측 금지(fail-honest).
- PII 없음(회사코드·담보코드·금액·보험료만).
- N2/N6 판정은 공용 헬퍼 `classifyScaleExclusion` 하나를 공유 — 두 곳의 사유 판정이 구조적으로 일치.

### F5. `dom_selection_hash` summary 노출 (실제 누락 버그 수정)
`runRealInjection` sink 에서 `summary.dom_selection_hash = domDiag.dom_selection_hash;` 추가. `scaled_excluded_reasons_total`·`row_evidence` 도 함께 복사.

---

## L1 스모크테스트 결과

- **서버 재시작: 해당없음** — 브라우저 확장 진단 코드(서버 프로세스 없음).
- **API 응답 확인: 해당없음(HTTP)** — 대신 **실제 jsdom DOM + 실제 형태 API 페이로드로 전체 경로 실행**.
- **스크린샷: 해당없음** — 화면 UI 변경 0. 대신 아래 실출력을 증거로 첨부.
- 스모크 스크립트: `/home/jay/.cokacdir/workspace/A8EA20F1/smoke_2787_7.cjs` (레포 외부, 커밋 대상 아님)
- 실행 방식: `#bojang_lists` 체크박스 3개(c001/c002/c003) + 미체크 1개, `#premium_lists` em 셀 4개(비선택 `cX` 포함), 2개사 API 페이로드.

### 실출력 (발췌 — 실제 실행 결과)

DOM 스냅샷:
```
checked=3  cds=[c001, c002, c003]
dom_amounts[0]={"cd":"c001","amount_input":"10,000","input_id":"input_a001","row_editable_input_count":1}
```

per_insurer_dom (M01):
```
join_summable: 2, dom_selected_adapter_sum: 1500      ← 기존 필드
screen_total: 12777, classification: OUT_OF_TOLERANCE  ← 기존 필드
dom_selected_scaled_sum: 11000                         ← 신규
scaled_included_count: 2                               ← 신규
screen_selected_total: 12000                           ← 신규
scaled_diff: 1000, scaled_classification: OUT_OF_TOLERANCE  ← 신규
scaled_excluded_reasons: {zero_premium_unverified: 1, 나머지 8종 0}
```

per_insurer_dom (S02) — 정상 EXACT 경로 재현:
```
dom_selected_scaled_sum: 9000, screen_selected_total: 9000
scaled_diff: 0, scaled_classification: EXACT
scaled_excluded_reasons: {api_missing: 2, 나머지 0}
```

전체 제외사유 집계 / 해시:
```
scaled_excluded_reasons_total = {api_missing:2, zero_premium_unverified:1, 나머지 6종 0}
dom_selection_hash = 4db212a6
```

row_evidence (6행 = 2사 × 3담보, 상한 없음). 표본 2행:
```
{company_code:"M01", cd:"c001", api_premium:1000, api_base_amount:1000,
 api_guide_premium:1000, api_ratio:1, dom_amount_raw:"10,000", dom_amount_parsed:10000,
 scaled_value:10000, screen_cell_premium:10000, screen_cell_text:"10,000",
 diff_scaled_vs_cell:0, excl_reason:null}

{company_code:"S02", cd:"c002", api_premium:null, api_base_amount:null,
 api_guide_premium:null, api_ratio:null, dom_amount_raw:"2,000", dom_amount_parsed:2000,
 scaled_value:null, screen_cell_premium:"UNKNOWN_NO_CELL",
 screen_cell_text:"UNKNOWN_NO_CELL", diff_scaled_vs_cell:null, excl_reason:"api_missing"}
```

**L1 판정: 통과** — 신규 필드 전부 실제 DOM에서 실값 산출, `UNKNOWN_NO_CELL` fail-honest 경로 실동작 확인.

---

## 기존 필드 무변경 회귀 증거 (실행 결과)

1. **diff 증거**: `git diff HEAD~2 -- extension/inject.js` → **314 insertions, 1 deletion**. 삭제된 유일한 1줄은 `const perInsurerDom = joined.per_company.map((c) => {` (동일 내용으로 재작성된 map 시작줄). 기존 함수 `extractDomCheckedSelection` / `joinDomSelectionToApi` / `classifyScreenTotalMatch` / `computeDomSelectionHash` / `observeApiAmountForCd` / `scrapeScreenTotals` 는 시그니처·본문 모두 diff에 등장하지 않음(grep 검증 0건).

2. **런타임 증거** (스모크 실행 결과):
```
[회귀] screenCells 유무에 따른 기존필드 동일: true
[회귀] hash 동일: true
[회귀] 기존필드 실값: join_matched/join_summable/dom_selected_adapter_sum/
       screen_total/classification/tolerance_basis/dom_selected_coverage_count/diff
       → screenCells 전달 여부와 무관하게 완전 동일
```
즉 `screenCells` 인자를 전달하지 않는 기존 호출 형태에서도 기존 필드가 동일하게 산출된다.

3. **테스트 증거**: 기존 37건 전부 수정 없이 통과.

---

## 테스트 결과 (팀장 직접 재실행)

- `npx vitest run extension/__tests__/dom_selection_snapshot.test.ts` → **1 file passed / 77 tests passed, 0 failed**
  - 기존 37건 + 신규 40건 (콤마 파싱 8 / 스케일 계산 3 / 제외사유 9종·항상존재·null 13 / oracle 필터링 5 / row_evidence 6 / hash 2 / 기존필드 무변경 회귀 3)
- `npx vitest run` (전체) → **Test Files 36 passed (36) / Tests 509 passed (509)**
- goal_assertions 2건 모두 충족.

---

## MATCH / GAP / UNKNOWN

**MATCH**
- 제외사유 9종이 각각 독립적으로 관측 가능하며, 어떤 행도 조용히 통과하지 않음(테스트 13건으로 확인).
- `screen_total`(전체합)과 `screen_selected_total`(선택필터) 이 실제로 다른 값을 낸다는 것을 실행으로 재현 — 12777 vs 12000, 차이의 정체는 비선택 담보 셀(`cX`=777) + 픽스처 구성분. **회장 실측 `screen_total=629,584` 가 "선택 필터 부재만으로도 설명 가능"하다는 가설과 구조적으로 부합**한다.
  - 단, 이는 픽스처 기반 **구조 재현**이지 실측 확증이 아니다. 확증은 다음 self-smoke 의 `screen_selected_total` 실값으로만 가능.
- `dom_selection_hash` 가 summary 에 실제로 노출됨(F5 버그 수정 확인).

**GAP**
- 2.8% 잔여차의 실제 원인은 **아직 미확정**. 이번 작업은 원인을 특정한 것이 아니라, 원인을 특정할 수 있는 **관측 장비를 설치**한 것이다. 다음 self-smoke 실행 시 `row_evidence` 의 `diff_scaled_vs_cell` 이 0이 아닌 행들을 보면 원인 행이 직접 드러난다.

**UNKNOWN (단정 금지)**
- `em[premium]` 이 화면 스케일값인지 API 기준값인지 — 여전히 UNKNOWN. `row_evidence` 의 `screen_cell_premium` vs `api_premium` vs `scaled_value` 3자 비교로 다음 라운드에 판정 가능하나, 이번 라운드에서 단정하지 않는다.
- 실제 10개사 환경에서 `row_evidence` 약 320행의 콘솔 출력 규모가 실용적인지는 실브라우저 미검증(구조적으로는 문제 없으나 실측 필요). STOP_REPORT 사유에는 해당하지 않는다고 판단(콘솔 객체 출력이며 직렬화 전송 없음).

---

## 불변식 준수 확인

- [x] 기존 `user_coverages` 경로 / `per_insurer`(비-dom) / `selected_coverage_count` / `dom_selected_adapter_sum` 계산식 / `screen_total` / `classification` **전부 무변경** (diff + 런타임 + 테스트 3중 확인)
- [x] 신규는 전부 병렬 필드
- [x] 진단게이트(`__INSURO_DIAG_ACTIVE__===true`) 내부에서만 실행 — 게이트 조건 자체는 diff 무변경, 프로덕션 무동작
- [x] 스케일 결과를 production 계산원으로 승격하지 않음 (관찰 전용)
- [x] 신규 파일 0 (`new_file_limit: 0` 준수)
- [x] forbidden_paths 미접촉 (`src/**`, `server/**`, `supabase/**`, `content.js`, `background.js`, `manifest.json`, `build-diagnostic.mjs` 전부 무수정)

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|---|---|---|---|
| /home/jay/projects/InsuRo/.worktrees/task-2787+2-dev5/extension/inject.js | 스케일 합계·선택기준 oracle·row-level 증거 병렬필드 + dom_selection_hash summary 노출 | grep "dom_selected_scaled_sum" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2787+2-dev5/extension/inject.js | row_evidence 배열 + UNKNOWN_NO_CELL fail-honest | grep "UNKNOWN_NO_CELL" OK | verified |
| /home/jay/projects/InsuRo/.worktrees/task-2787+2-dev5/extension/__tests__/dom_selection_snapshot.test.ts | 신규 40건 테스트 + 기존필드 무변경 회귀 assert | grep "scaled_excluded_reasons" OK | verified |

## 머지 판단

- **머지 필요: No** — task 파일이 PR·merge·배포·운영활성화를 명시적으로 금지(`merge_policy: none`). same-branch 커밋만 수행.
- **브랜치**: `task/task-2787+2-dev5`
- **워크트리**: `/home/jay/projects/InsuRo/.worktrees/task-2787+2-dev5`
- **머지 의견**: 진단 관찰 전용 코드이며 프로덕션 경로 무동작. 머지 판단은 회장/아누 몫으로 남긴다.

## 커밋

- `ca3ab84` — [task-2787+7] 엔키: 스케일 합계·선택기준 oracle·row-level 증거 병렬필드 추가 + dom_selection_hash summary 노출
- `f282693` — [task-2787+7] 닌기르수: 스케일·제외사유9종·row_evidence·oracle 테스트 + 기존필드 무변경 회귀 assert

## 발견 이슈 및 해결

1. **F5 `dom_selection_hash` summary 누락** — task 지시대로 실재 버그였음. `runRealInjection` sink 에 복사 추가로 해결. 스모크 출력에서 값(`4db212a6`) 실제 산출 확인.
2. **N2/N6 사유 판정 중복 구현 위험** — 두 곳에서 각각 판정하면 발산 가능. 공용 헬퍼 `classifyScaleExclusion` 하나로 통합해 구조적으로 방지.
3. **`node -e require()` 로드 불가** — 레포 `package.json` 이 `"type": "module"` 이라 CJS require 실패. 이는 이번 변경과 무관한 기존 환경 특성으로, `git stash` 로 원본에서도 동일함을 확인. `node --check` + vitest 실행 + vm 기반 스모크로 대체 검증.

4. **[완료 게이트] QC evidence root 오지정으로 `.qc-result` 미생성** — `finish-task.sh` 에 project_path 를 넘기지 않으면 QC evidence root 가 `/home/jay/workspace` 로 fallback 되어, git_evidence 가 workspace repo 에서 task 커밋을 찾다가 0건 → FAIL → `.qc-result` 미작성. PROJECT_PATH 를 worktree 로 지정해 재실행하여 해소(git_evidence PASS). 원인은 재현 실험으로 확정(workspace root 지정 시 FAIL 재현, worktree 지정 시 PASS).

5. **[완료 게이트] scope-guard 6건 오탐 (구조적, 3회째 반복)** — scope-gate 의 base 가 `merge-base(origin/main, HEAD)` 라, 미머지 same-branch 위에 Phase 를 누적하는 이 작업 구조에서는 **이전 Phase·타 task 의 파일까지 이번 delta 로 재계산**된다. 지목된 6건 전부 `git log -1` 로 타 커밋 소유임을 확인했고, 이번 커밋 2건(ca3ab84/f282693)은 그중 어느 것도 건드리지 않았다. phase base(3c38303 = +6 HEAD) 기준으로 scope-guard 를 **실제 재실행하여 PASS(2 files in scope, exit 0)** 확인 후 판정 기록. bypass 아님.
   - 감사 노트: `memory/events/task-2787+7.scope-base-note.md`
   - 재검증 diff: `memory/events/task-2787+7.scope-diff-phasebase.txt`
   - ★ **아누 판단 요청**: +5, +6, +7 로 3회 연속 동일 오탐. 브랜치 머지 전까지 이후 Phase 에서도 계속 재발한다. scope-gate base 를 `.worktree-base.json` 우선으로 삼는 개선 검토 필요.

6. **[완료 게이트] GOAL-GATE goal_assertions TIMEOUT 오탐** — 게이트가 `npx vitest run` 을 **workspace 를 cwd 로** 실행해 vitest 미설치 상태에서 30초 타임아웃(rc=124). worktree 를 cwd 로 두면 rc=0 임을 대조 실험으로 확인(workspace rc=124 / worktree rc=0). `cd <worktree> && bash finish-task.sh ...` 로 재실행하여 해소. goal_assertions 2건은 팀장이 worktree 에서 직접 실행해 PASS(77건 / 509건) 확인 완료.

## 완료 상태

- `.done` 생성됨: `memory/events/task-2787+7.done` (qc_result=WARN, gate_results 전부 PASS)
- QC WARN 사유: `claude_md_check` — `design/CLAUDE.md` 310줄(100줄 초과). 이번 변경과 무관한 기존 문서 길이 경고.
- result.json: `/home/jay/projects/InsuRo/memory/events/p0b_inbox/task-2787+7.result.json` (owner_pickup_required=true, executor self-key callback 발사 0)
- 머지 미수행 확인: `[FINALIZE-ONLY] merge block 스킵` — merge_policy=none 준수, PR 생성 0건.

## 모델 사용 기록

- 엔키(백엔드) — sonnet: inject.js 신규 순수함수 구현. haiku 미사용(로직 정확성 요구).
- 닌기르수(테스터) — sonnet: 테스트 40건 추가. haiku 미사용(제외사유 9종 판정 로직 이해 필요).
- 이쉬타르(프론트)·나부(UX/UI) — 미소집: UI 변경 0인 관찰 필드 작업이라 역할 해당 없음.
- 마르둑(팀장, Opus) — 직접 코딩 없음. 설계 명세 확정 / diff 직접 검증 / vitest 직접 재실행 / L1 스모크 직접 수행.

## 검증 방법 명시 (인지 검증)

팀원 보고를 그대로 신뢰하지 않고 팀장이 직접 재실행한 항목:
1. `git diff --stat` + 삭제 라인 전수 확인 → 기존 코드 훼손 여부 직접 확인 (삭제 1줄 검증)
2. `npx vitest run` 2회 직접 실행 → 77 / 509 숫자 직접 확인
3. jsdom 실행으로 신규 필드 실값 직접 확인 → 보고서의 모든 출력값은 실제 실행 결과 복사본
4. `screenCells` 유무 2회 호출 비교로 기존 필드 무변경을 **런타임에서** 확인 (코드 리뷰만으로 단정하지 않음)
