# Named manual restore read-only audit

감사일: 2026-07-12 (KST)

범위는 운영 Oracle/MariaDB의 `SELECT`와
`%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\OperatorData`의 production read path뿐이다.
원본 저장소, 운영 DB, 운영 파일, import marker/intent, Tornado2에는 쓰기나 명령을
보내지 않았다. 감사 출력에는 program code, title, stock name/code, VI 이름 목록,
파일 경로, 해시 또는 인증 정보를 포함하지 않는다.

## 현재 PList 문법 분포

실제 `DC_LIST`/`PLAY_LIST` 2,213행을 lossless parser로 읽고
`LegacyPListAuditRoute.ManualFreshMaterialization` 36행을 독립 closed grammar로 다시
분류했다.

| 종류 | 행 | 세부 분포 |
| --- | ---: | --- |
| GraphE | 36 | 주요매출 구성 12, 매출액 12, 영업이익 12 |
| GraphE 성장성 지표 | 0 | 현재 저장 행 없음 |
| FSell 순매도 | 0 | 현재 저장 행 없음 |
| VI 수동 목록 | 0 | 현재 저장 행 없음 |
| 합계 | 36 | 역사형 36, canonical형 0 |

따라서 이전 감사 문구의 `GraphE/FSell/VI 36행`은 “세 manual 복원 경로가 차단
대상”이라는 범주명이었고, 실제 36행의 내용은 전부 GraphE였다. FSell/VI 경로는
구현돼 있으나 현재 named PList에는 해당 행이 없다.

분류기는 Web의 `named-manual-restore-workflow.js`와 같은 두 문법을 닫힌 값으로
허용한다.

| 종류 | 역사형 | fresh materialize 후 canonical형 |
| --- | --- | --- |
| GraphE | 한국어 시장 + 원본 GraphE 화면명 + 종목 표시명 + `1/1` | canonical 시장 + canonical 화면명 + 동일 표시명 + `1/1` |
| FSell | 수동 순매도 3개 audience + 지수/순매도 + `1/1` | `INDIVIDUAL`/`FOREIGN`/`INSTITUTION` + `MANUAL_NET_SELL` |
| VI | 쉼표 구분 이름 목록 + 계산된 `1/N` | `PAGED_VI` + reserved snapshot subject + lowercase SHA-256 version |

## 실제 fresh materialization 결과

GraphE는 브라우저/네이티브 복원 순서를 그대로 축약해 다음 증거를 요구했다.

1. `LegacyManualFinancialScreenService.GetAsync` 첫 조회
2. 현재 종목 마스터 검색(브라우저와 같은 최대 200건, truncated 금지)
3. 같은 GraphE identity를 두 번째로 조회
4. 두 조회의 64자리 Oracle row version이 동일한지 확인
5. 시장·provider·종목 code가 유일한지 확인
6. `ManualFinancialCutContracts.CreateSelection` 결과가 화면별 cut 계약과 일치하는지 확인

| 화면 | 후보 | fresh materialize 가능 | 차단 |
| --- | ---: | ---: | ---: |
| 주요매출 구성 | 12 | 5 | 7 (`GRAPH_E_DATA_INVALID`) |
| 매출액 | 12 | 6 | 5 (`GRAPH_E_AMBIGUOUS`), 1 (`GRAPH_E_DATA_INVALID`) |
| 영업이익 | 12 | 11 | 1 (`GRAPH_E_DATA_INVALID`) |
| 합계 | 36 | 22 | 14 |

`GRAPH_E_DATA_INVALID` 9행은 production GraphE parser가 현재 INPUT_* 저장값을
typed record로 만들 수 없는 경우다. `GRAPH_E_AMBIGUOUS` 5행은 같은 화면에서
하나의 저장 identity가 둘 이상 조회되어 원본 schema의 이름-only identity로는
어느 행인지 결정할 수 없는 경우다. 원시 이름과 저장값은 출력하지 않았다.

22행의 성공은 “현재 raw PList 행을 fresh GraphE selection으로 복원 가능”하다는
뜻이다. 각 scene의 시세 조회, cut asset, PREPARE 또는 Tornado/PGM 송출 성공을
의미하지 않는다.

## FSell/VI production source 확인

현재 named PList 후보는 0행이지만 향후 저장/복원을 막는 source 문제인지 별도로
검사했다.

- runtime guard: `ReadyLegacyData`
- FSell: 세 audience 모두 production CP949 parser로 opened-handle read 성공,
  audience마다 정확히 5행
- VI: production opened-handle read 성공, 9개 항목, 2 page, versioned 두 번째 read 일치
- 운영 디렉터리: 4개 data entry가 감사 전후 이름·크기·생성/수정 시각·SHA-256 모두 동일
- marker/intent: 감사 전후 생성 없음
- writes attempted: `false`

`ReadyLegacyData`인 현재 profile에 byte-identical 4파일이 이미 존재하므로 importer를
실행하거나 완료 marker를 임의로 생성하지 않는다. 이번 결과는 기존 trusted store의
read 가능성만 증명한다.

## Native request 대응

| 복원 종류 | fresh proof에 사용되는 native request |
| --- | --- |
| GraphE | `request-manual-financial-load` → `search-stocks` → `request-manual-financial-selection` |
| FSell | `request-manual-net-sell-data` |
| VI | `request-vi-manual-list` 후 반환 version을 reserved snapshot reference로 고정 |

구현 API는
`src/MBN_STOCK_WEBVIEW.Core/Data/LegacyNamedManualRestoreAudit.cs`의 closed classifier와
pure fresh-proof evaluator, 실행 경계는
`tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedManualRestoreReadOnlyAudit.cs`의
`NamedManualRestoreReadOnlyAudit.RunAsync`다. 실행 경계는 root smoke에서 다음처럼
호출할 수 있다.

```csharp
await NamedManualRestoreReadOnlyAudit.RunAsync(
    runtime.Executor,
    NamedManualRestoreReadOnlyAudit.DefaultTrustedDirectory(),
    cancellationToken);
```

집중 Core 테스트 21건은 역사형/canonical GraphE·FSell·VI grammar, NXT 표시명 검색,
GraphE double-read version, master identity 중복, FSell 5행, VI version/name/page drift를
검증한다.

## 남은 조치

1. `GRAPH_E_DATA_INVALID` 9행은 운영자가 승인한 snapshot을 먼저 만들고 각 INPUT_*
   원본 필드 형식을 확인한다.
2. `GRAPH_E_AMBIGUOUS` 5행은 이름-only identity 중 어느 행을 유지할지 운영자가
   결정해야 한다. 감사 코드가 임의 삭제하거나 첫 행을 선택하지 않는다.
3. 수정은 별도의 운영 DB write 승인, 대상 identity, rollback 계획과 OutcomeUnknown
   규칙을 갖춘 회차에서만 수행한다.
4. 수정 후 같은 read-only 감사를 다시 실행해 36/36 fresh materialization을 확인한다.
5. 그 다음 scene data/asset DryRun을 확인하고, 실제 PGM TAKE IN은 별도 Gate A/B
   승인 회차에서만 수행한다.
