# PList 기존 데이터 읽기 전용 동등성 감사

기준일: 2026-07-12

> 2026-07-22 현행 `LegacyParityApp` 보충 판정: 아래 2026-07-12의 `1,603/610`은 당시
> raw 행에서 current typed selection을 fresh하게 증명한 역사적 수치이며, DB 재생목록
> 자체의 로드 성공/실패 수치가 아니다. 최신 operator 호환 감사에서는 활성 정의 23개와
> 2,213행을 모두 로드했고(`wholeListRejected=0`), 2,213행 모두 기존 scene alias로
> 식별됐다. 최신 실제 DB compatibility projection은 GraphE 36행을 36/36 복원해 현재
> `1,617 복원 / 596 보류`다. 최신 등록 개발 AppX에서도 `DB 불러오기 → 첫 목록 선택 →
> 불러오기`를 실제 마우스로 실행해 목록 로드·선택·스케줄 추가와 관련 화면/DB 동작을
> 최종 물리 입력 회차에 포함해 확인했다. 다만 이
> 감사는 각 행의 asset·Tornado2 PREPARE/PGM 성공까지 주장하지 않는다
> (`playoutPreflightAsserted=false`).

원본(읽기 전용): `C:\Users\MD\source\repos\MBN_STOCK_N\MBN_STOCK_N\Form\PList.cs`
감사 구현:
[LegacyPListReadAuditParser](../src/MBN_STOCK_WEBVIEW.Core/Data/LegacyPListReadAudit.cs),
[NamedPlaylistReadOnlyDbAudit](../tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedPlaylistReadOnlyDbAudit.cs),
[NamedManualRestoreReadOnlyAudit](../tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedManualRestoreReadOnlyAudit.cs),
[LegacyManualFinancialStorageCompatibility](../src/MBN_STOCK_WEBVIEW.Core/Playout/Scenes/LegacyManualFinancialStorageCompatibility.cs),
[NamedPlaylistOperatorCompatibilityAudit](../tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedPlaylistOperatorCompatibilityAudit.cs)

## 원본 읽기 의미

원본 `PList.data_load`는 `PLAY_LIST`를 `ITEM_INDEX` 순으로 읽고
`LIST_TEXT`를 `^`로 나눈 뒤 FarPoint의 0~6열에 넣는다. 첫 필드는 `1`일 때만
활성이고, 여섯 번째 필드인 PageN 문자열에서만 ASCII 공백을 제거한다. 다른
필드는 공백·한글·기호를 포함해 변경하지 않는다.

새 감사도 이 경계를 그대로 사용한다.

- 정확히 7필드인지 검사한다.
- 원문을 다시 `^`로 결합했을 때 DB 문자열과 code-unit 단위로 같아야 한다.
- 페이지 표시값 이외의 필드는 정규화하지 않는다.
- 프로그램 코드는 8자리 숫자, `ITEM_INDEX`는 0부터 연속이어야 한다.
- 제어문자, U+FFFD replacement 문자와 알려진 UTF-8/codepage 손상 신호를
  거부한다.
- 같은 프로그램을 현재 `LegacyNamedPlaylistPersistenceService.LoadAsync`로
  다시 읽어 활성값, 5개 selection 필드와 페이지 표시값을 모두 대조한다.

감사 오류와 정상 출력에는 `DC_TITLE`, `LIST_TEXT`, 종목명, 테마명, 프로그램
코드 또는 인증정보를 포함하지 않는다.

## 2026-07-22 현행 fresh-selection 재감사 결과

최신 실제 Oracle/MariaDB smoke는 원본 저장 의미와 current typed projection을 다시 대조했다.
숫자 token의 선후행 공백·TAB, 분기명 후행 TAB 및 모든 필드가 완전히 같은 중복 DB행은 원본과
같은 값으로 수용한다. 내부 제어문자와 서로 충돌하는 중복행은 계속 거부한다. 이 경계로 과거
보류됐던 GraphE 14행까지 모두 fresh materialization됐으며 임의 종목·장면 추정은 없다.

| 항목 | 현재 결과 |
|---|---:|
| 활성 `DC_LIST` 정의 | 23 |
| 연결된 `PLAY_LIST` 행 / 로드 / scene alias 식별 | 2,213 / 2,213 / 2,213 |
| whole-list reject | 0 |
| fresh typed selection 복원 | 1,617 |
| fresh selection 보류 | 596 |
| 이름 기반 GraphE fresh materialization | 36/36 |

| route | 전체 | current 복원 | 보류 |
|---|---:|---:|---:|
| 기존 닫힌 매퍼 | 1,250 | 1,250 | 0 |
| 업종 fixed 닫힌 매퍼 | 24 | 24 | 0 |
| 비교 fresh identity | 99 | 74 | 25 |
| 해외지수 candle fresh identity | 61 | 61 | 0 |
| NXT theme fresh identity | 48 | 0 | 48 |
| KRX theme fresh identity | 587 | 94 | 493 |
| 해외종목 fresh identity | 78 | 78 | 0 |
| GraphE fresh materialization | 36 | 36 | 0 |
| unique closed mapper 없음 | 30 | 0 | 30 |
| 합계 | **2,213** | **1,617** | **596** |

현재 596행의 보류 원인은 KRX 테마 493행, NXT 테마 48행, 비교 시장 identity 유실 25행,
unique closed mapper 없음 30행뿐이다. 이 수치는 목록 로드 실패가 아니며, current selection을
하나로 증명하지 못한 행을 nearest scene으로 추정하지 않았다는 뜻이다.

## 2026-07-12 역사적 fresh-selection 감사 결과

실제 운영 연결에 고정 SQL과 bind 변수로 `SELECT`만 실행했다. DB write,
Tornado2 명령과 원본 저장소 변경은 수행하지 않았다.

| 항목 | 결과 |
|---|---:|
| 활성 `DC_LIST` 정의 | 23 |
| 연결된 `PLAY_LIST` 행 | 2,213 |
| 정확히 7필드로 parse | 2,213 |
| 원문 무손상 round-trip | 2,213 |
| replacement/mojibake 신호 | 0 |
| 비연속 `ITEM_INDEX` 프로그램 | 0 |
| 현재 persistence 결과 불일치 | 0 |
| 단일 route와 결과로 분류 | 2,213 |
| 기본 감사에서 `selectionMapped` | 1,581 |
| 기본 감사에서 안전 차단 | 632 |
| 별도 GraphE fresh proof에서 추가 `selectionMapped` | 22 |
| 별도 GraphE fresh proof에서 안전 차단 | 14 |
| 두 read-only 증거를 합친 `selectionMapped` | 1,603 |
| 두 read-only 증거를 합친 안전 차단 | 610 |

`단일 route와 결과로 분류`는 모든 행이 송출 가능하다는 뜻이 아니다. 기본
`NamedPlaylistReadOnlyDbAudit`는 KRX·해외종목 복원까지 적용해 1,581행을 단일
current selection으로 매핑하고 632행을 차단한다. 이 632행에는 별도 production
source 증거가 필요한 manual 36행이 들어 있다. `NamedManualRestoreReadOnlyAudit`가
그 36행을 독립적으로 다시 읽어 GraphE 22행을 fresh selection으로 만들고 14행을
차단하므로, 두 read-only 증거를 행별로 합친 결과는 1,603행 `selectionMapped`,
610행 차단이다.

`selectionMapped`는 raw 7필드에서 현재 typed selection을 하나 만들었다는 뜻만
갖는다. 각 장면의 시세·재무 데이터 결과, 컷·배경 자산, package context,
PREPARE 성공과 Tornado2/PGM 출력은 포함하지 않으며 각각 별도 gate를 통과해야
한다.

### route 집계

| route | 전체 | `selectionMapped` | 차단 |
|---|---:|---:|---:|
| 기존 닫힌 매퍼 | 1,250 | 1,250 | 0 |
| 업종 fixed 닫힌 매퍼 | 24 | 24 | 0 |
| 비교 fresh identity | 99 | 74 | 25 |
| 해외지수 candle fresh identity | 61 | 61 | 0 |
| NXT theme fresh identity | 48 | 0 | 48 |
| KRX theme fresh identity | 587 | 94 | 493 |
| 해외종목 fresh identity | 78 | 78 | 0 |
| GraphE fresh materialization | 36 | 22 | 14 |
| unique closed mapper 없음 | 30 | 0 | 30 |
| 합계 | 2,213 | 1,603 | 610 |

기본 감사 수치 1,581은 기존 닫힌 매퍼 1,250행, 업종 fixed 24행, 비교
74행, 해외지수 candle 61행, KRX theme 94행 및 해외종목 78행의 합이다.
GraphE 22행은 별도 fresh proof의 성공분이다. 해당 회차 PList의 manual 36행은 모두
GraphE이고 FSell/VI 저장 행은 0건이다.

기존 fixed 매퍼는 임의 문자열 허용으로 판정하지 않았다. 원본 runtime
`해외.ini`·`환율.ini`·`지수.ini`를 각각 현재 Web 계약에 고정된 SHA-256으로
검증하고, 원본 `MainForm`과 같이 각 줄을 trim한 뒤 현재
`fixedLegacySignature`의 exact 5필드 규칙으로 변환했다. 원본 INI의 328 source action에서
당시 runtime action으로 실체가 없는 수동 placeholder 6개를 제외하고, 같은 raw 서명으로 겹치는
후보는 unique로 인정하지 않았다. 이 exact whitelist와 일치하지 않은 30행은
가까운 `s5001` 등을 추정하지 않고 차단했다.

### 당시 fresh restore 결과와 차단 사유

| 사유 | 행 | 의미 |
|---|---:|---|
| comparison market identity 유실 | 25 | 원본 2열판이 KOSPI/KOSDAQ/NXT/해외 구분을 저장하지 않았다. |
| KRX theme identity 없음 | 271 | 원본 제목 exact lookup으로 현재 유일한 KRX identity를 찾을 수 없다. |
| KRX theme 저장 행 무효 | 16 | 닫힌 5·6·12행 문법과 필수 field를 만족하지 않는다. |
| KRX theme preview empty | 193 | 현재 제목/code는 찾았지만 validated live item이 없다. |
| KRX theme 지원 불가 action | 13 | 현재 5·6·12행 장면 계약으로 유일하게 해석할 수 없다. |
| GraphE 현재 저장값 무효 | 9 | production GraphE parser가 현재 INPUT_* 저장값을 typed record로 만들 수 없다. |
| GraphE 이름 identity 중복 | 5 | 원본 이름-only identity로 현재 저장 행 하나를 결정할 수 없다. |
| NXT theme preview empty | 48 | 현재 title/code 매핑은 성공했으나 MariaDB validated preview가 비어 있다. |
| unique closed mapper 없음 | 30 | hash가 고정된 328-action runtime 계약의 어느 단일 서명과도 일치하지 않아 nearest scene을 추정하지 않는다. |

KRX theme 587행 중 94행은 제목 exact lookup, 현재 code 및 validated preview를
같은 correlated batch에서 확인해 모두 현재 code로 remap했다(`codeRemapped=94`).
나머지 493행은 위 네 KRX 사유로 차단했다. 해외종목 78행은 현재 Oracle의 exact
입력명에서 유일한 symbol/nation을 얻고 original action 문법을 보존해 78/78을
매핑했다. 두 경로 모두 timeout/불명확 응답을 재시도하지 않으며 raw 이름·code를
감사 출력에 기록하지 않는다.

당시 GraphE 36행의 별도 감사는 같은 identity를 두 번 읽어 64자리 row version이
변하지 않는지 확인하고 현재 종목 master의 유일한 identity와 cut 계약을 함께
검증했다. 22행만 materialize했고 저장값 무효 9행과 identity 중복 5행은 첫 후보를
고르지 않고 차단했다. 상세 경계는
[manual read-only 감사](NAMED_MANUAL_RESTORE_READ_ONLY_AUDIT.md)에 있다.

FSell/VI는 현재 PList 후보가 0행이지만 운영 production source를 opened-handle로
별도 읽었다. FSell은 3 audience마다 5행, VI는 9항목·2 page였고 두 번째 VI read의
version도 같았다. 네 운영 파일의 이름·크기·시각·SHA-256은 감사 전후 동일했고
import marker/intent를 만들거나 파일을 쓰지 않았다.

2026-07-22 현재 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\OperatorData`의 `개인`, `외국인`,
`기관`, `VI발동` 4파일은 authoritative 원본 `bin\Debug\Data` 파일과 각각 length 및
SHA-256이 정확히 같다. 따라서 이 수동 파일은 미이관 또는 unavailable이 아니라 현재 앱에서
사용 가능한 로컬 운영 데이터다. 원본과 로컬 파일 모두 이 감사에서 수정하지 않았다.

## 구현과 자동 근거

- KRX theme:
  [Core restore](../src/MBN_STOCK_WEBVIEW.Core/Data/LegacyNamedKrxThemeRestore.cs),
  [native bridge](../MainWindow.NamedKrxThemeRestore.cs),
  [Web workflow](../Web/named-krx-theme-restore-workflow.js),
  [Core tests](../tests/MBN_STOCK_WEBVIEW.Core.Tests/LegacyNamedKrxThemeRestoreServiceTests.cs),
  [Web tests](../tests/Web/named-krx-theme-restore-workflow.test.cjs),
  [app integration](../tests/Web/named-krx-theme-restore-app-integration.test.cjs)
- 해외종목:
  [Core restore](../src/MBN_STOCK_WEBVIEW.Core/Data/LegacyNamedForeignStockRestore.cs),
  [native bridge](../MainWindow.NamedForeignStockRestore.cs),
  [Web workflow](../Web/named-foreign-stock-restore-workflow.js),
  [Core tests](../tests/MBN_STOCK_WEBVIEW.Core.Tests/LegacyNamedForeignStockRestoreServiceTests.cs),
  [Web tests](../tests/Web/named-foreign-stock-restore-workflow.test.cjs),
  [app integration](../tests/Web/named-foreign-stock-restore-app-integration.test.cjs)
- manual:
  [closed grammar/fresh proof](../src/MBN_STOCK_WEBVIEW.Core/Data/LegacyNamedManualRestoreAudit.cs),
  [Web workflow](../Web/named-manual-restore-workflow.js),
  [read-only runner](../tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedManualRestoreReadOnlyAudit.cs),
  [Core tests](../tests/MBN_STOCK_WEBVIEW.Core.Tests/LegacyNamedManualRestoreAuditTests.cs),
  [Web tests](../tests/Web/named-manual-restore-workflow.test.cjs)

## 반복 실행

전체 실제 DB smoke에 이 감사가 포함된다.

```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj `
  -c Release --no-restore
```

감사는 실패 시 원문을 출력하지 않고 boundary 이름만 남긴다. timeout 또는
불명확 결과를 재시도하거나 반대 명령으로 보정하지 않는다.

## 자동 검증

`LegacyPListReadAuditParserTests`는 다음을 고정한다.

- 한글·공백·빈 필드를 포함한 7필드 무손상 round-trip
- 페이지 필드에만 적용되는 원본 ASCII 공백 제거
- field 수, enabled 값, 프로그램 코드, item index 경계
- replacement와 codepage 손상 신호 차단
- typed persistence 대조와 연속 index
- comparison/업종/해외지수/NXT·KRX theme/해외종목/manual route 분리
- KRX current-code correlated remap과 5·6·12행 문법, 해외종목 symbol/nation
  correlation, GraphE double-read row-version·identity 중복 fail-closed
- exact stock label과 nearest-label 거부, 알 수 없는 행의 `Unmapped` fail-closed

실제 기본 감사, 별도 manual fresh 감사와 전체 DB smoke는 `REAL_DB_SMOKE: PASS`로
끝났다. 2026-07-22 최신 전체 DB smoke도 PASS이며 current 결과는 GraphE 7,726/50,
named playlist 1,617/596, GraphE fresh 36/36이다. DB write와 Tornado2/PGM 명령은
0건이며 raw 값이나 인증정보도 출력하지 않았다.
