# 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 승인 회차에서만 수행한다.