feat: restore legacy named playlist workflows

This commit is contained in:
2026-07-12 11:59:18 +09:00
parent a481c96c39
commit af80c36cde
52 changed files with 9261 additions and 83 deletions

View File

@@ -0,0 +1,64 @@
# 외부 송출 자산 검색 감사 (2026-07-12)
## 결론
현재 장비의 `D:``E:`에서 아래 운영 대상 15개와 정확히 일치하는 실제
파일은 발견되지 않았다. 압축파일은 추출하지 않고 목차만 읽었으며, 그 안에서도
동일 이름의 항목은 발견되지 않았다. 따라서 다른 프로젝트의 `.vrv`나 테스트
fixture를 승인 Cuts 또는 배경 root에 복사해 대체해서는 안 된다.
- `큐브배경.vrv`
- `기본.vrv`
- `20201008_미국.vrv`
- `20201008_독일.vrv`
- `20201008_영국.vrv`
- `20201008_프랑스.vrv`
- `20201008_일본.vrv`
- `20201008_중국.vrv`
- `20201008_홍콩.vrv`
- `20201008_대만.vrv`
- `20201008_싱가포르.vrv`
- `20201008_태국.vrv`
- `20201008_필리핀.vrv`
- `20201008_말레이시아.vrv`
- `20201008_인도네시아.vrv`
## 검색 범위와 결과
검색일은 2026-07-12, 대상 볼륨은 로컬 `D:``E:`다. 검색은 읽기 전용으로
수행했고 `$RECYCLE.BIN``System Volume Information`은 최종 수치에서
제외했다. 조사 중 자산 파일 쓰기·복사·추출은 0건이다.
| 범위 | 조사 결과 | 정확한 대상 일치 |
| --- | ---: | ---: |
| `D:` 실제 `.vrv` | 233개 | 0개 |
| `E:` 실제 `.vrv` | 452개 | 0개 |
| `D:` ZIP | 2,967개 중 2,955개, 215,416 entry 읽음 | 0개 |
| `E:` ZIP | 7,313개 중 7,310개, 863,696 entry 읽음 | 0개 |
| `D:` 7z/RAR/TAR/TGZ | 93개 중 92개, 1,093 entry 읽음 | 0개 |
| `E:` 7z/RAR/TAR/TGZ | 5개 모두, 10,858 entry 읽음 | 0개 |
`MBN`, `V-Stock`, `매일경제`, `증권정보`, `MBN_STOCK` 경로명으로 별도 확인한
결과도 `D:`의 무관한 `MBN Candle그래프.PNG` 1개뿐이었고 `E:`는 0개였다.
읽지 못한 ZIP은 `D:`의 게임기 BIOS 12개, `E:`의 만화책 2개 및 아마야구 PSD
묶음 1개다. 읽지 못한 기타 archive 1개는 `D:`의 Adobe Photoshop 설치
RAR이다. 파일 경로와 용도가 이번 MBN 송출 자산과 무관하므로 이 예외가 대상
자산 존재 가능성을 남기지는 않는다.
## C: 선행 검색과의 합산 판정
선행 검색에서는 `C:`의 사용자/K3D/Tornado/ProgramData/Program Files 범위에
실제 대상 자산이 없었고, `C:\Tornado2`에는 무관한 `Weather.vrv` 1개만 있었다.
임시 폴더에서 발견된 대상명 1-byte 파일 14개는 자동 테스트 fixture이므로 운영
자산으로 사용할 수 없다.
이에 따라 현재 외부 의존성 판정은 그대로다.
- `s5006`: 승인 Cuts의 `Video\큐브배경.vrv` 제공 전 차단
- `s6001`: 승인 Cuts의 국가별 `Video\20201008_<국가>.vrv` 13개 제공 전 차단
- F2/F3 기본 배경: 별도 신뢰 배경 root의 `기본.vrv` 제공 전 차단
자산은 원 제작/운영 보관처에서 승인된 원본으로 제공받아야 하며, 제공 후 기존
cut coverage와 opened-handle/reparse/hardlink 검사를 다시 통과해야 한다.
외부에서 승인된 원본이 제공되기 전에는 위 세 경로를 모두 fail closed로 유지한다.

View File

@@ -29,17 +29,17 @@
| LF-006 | P1 | 시스템 종료 확인 | 완료 | `MainWindow.CloseConfirmation.cs`, `EnsureCloseConfirmationAttached`, `OnAppWindowClosing`, `BuildCloseConfirmationMessage`, `DetachCloseConfirmation`; 연결부 `MainWindow.xaml.cs:54-63,101-112,OnClosed`; 테스트 `tests/Web/operator-ui-app-integration.test.cjs:384-408` | 모든 시스템 close를 먼저 취소하고 기본 버튼이 취소인 확인창을 연다. PREPARED/PROGRAM, command pending, OutcomeUnknown에 맞는 경고를 표시하며 종료 과정에서 TAKE OUT이나 반대 명령을 자동 실행하지 않는다. | Web 통합 테스트를 실행하고 패키지 앱에서 idle, prepared/on-air 경고, 취소, 명시적 종료를 수동 확인한다. |
| LF-007 | P1 | 패키지 앱 단일 인스턴스 | 완료 | `App.xaml.cs:9-57`, `OnLaunched`, `OnMainInstanceActivated`; 테스트 `tests/Web/operator-ui-app-integration.test.cjs:72-88` | `AppInstance.FindOrRegisterForKey`가 MainWindow와 DB/WebView/playout runtime 생성 전에 실행된다. 두 번째 인스턴스는 activation을 primary로 전달한 뒤 종료하며 기존 프로세스를 이름으로 찾아 강제 종료하지 않는다. | 통합 테스트 후 설치된 MSIX를 두 번 실행해 프로세스 하나, 창 하나, 기존 창 활성화를 확인한다. |
| LF-008 | P1 | 운영자 모달의 전역 단축키 격리 | 완료 | `Web/app.js:6808-6845`, `hasOpenOperatorModal`, `isolateOperatorModalShortcut`; 테스트 `tests/Web/operator-ui-app-integration.test.cjs:410-429` | GraphE, FSell/VI, ThemeA/EList 등 modal이 열려 있으면 F2/F3/F8/Escape, Home/End/Delete/Space, Ctrl+K가 workspace 송출·playlist handler로 전달되지 않는다. F8/Escape에는 modal을 먼저 닫으라는 안내를 표시한다. | Web 통합 테스트 후 각 modal 입력 필드에서 해당 키를 눌러 playout command와 playlist mutation이 발생하지 않는지 확인한다. |
| LF-009 | P1 | PList의 기존 한국어 `DC_TITLE`/`LIST_TEXT` 복원 | 적용 중 | 원본 `MBN_STOCK_N/Form/PList.cs:81-124`, `data_load`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/NamedPlaylistPersistence.cs:442-540,742-787`, `LoadAsync`/`ParseItem`; `Web/named-playlist-workflow.js:334-412`, `restoreRawRows`; `Web/app.js:4317-4619,5017-5052` | 운영 Oracle read-only 집계에서 활성 정의 23개와 연결된 2,213행 모두 정확히 7필드canonical ASCII group은 0행, malformed/null은 0행이었다. 즉 전 행이 기존 한국어 형식이므로 legacy→canonical 복원이 필수다. 삭제된 정의에 연결된 orphan 88개 프로그램/9,369행은 현재 UI load 대상이 아니다. 닫힌 매퍼와 fresh identity 재검증을 보강 중이며 전체 2,213행의 unique/blocked 판정이 끝나기 전에는 완료로 보지 않는다. | 실제 read-only PList 행을 원본과 새 앱에서 같은 프로그램 코드로 읽어 `DC_TITLE`과 caret 7개 필드를 글자 단위로 비교한다. `<60>` 또는 mojibake가 없어야 하며 trusted restore와 fresh page plan 전에는 PREPARE가 계속 차단돼야 한다. raw 값은 로그·문서에 출력하지 않고 분류 건수만 기록한다. |
| LF-009 | P1 | PList의 기존 한국어 `DC_TITLE`/`LIST_TEXT` 복원 | 적용 중 | 원본 `MBN_STOCK_N/Form/PList.cs:81-124`, `data_load`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/LegacyPListReadAudit.cs`, `tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedPlaylistReadOnlyDbAudit.cs`, `tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedManualRestoreReadOnlyAudit.cs`; KRX/해외종목/manual 전용 restore와 테스트; 상세 `docs/NAMED_PLAYLIST_READ_AUDIT.md` | 실제 Oracle 활성 정의 23개와 2,213행 모두 정확히 7필드원문 재결합, 한국어 제목·필드, enabled, page 공백 제거 및 typed persistence가 일치했다. 기본 감사는 KRX 94행 current-code remap과 해외종목 78/78을 포함해 `selectionMapped=1,581`, 차단 632다. 별도 GraphE fresh 감사가 manual 36행 중 22행을 materialize하고 저장값 무효 9행·identity 중복 5행을 차단해 두 증거의 통합 결과는 `selectionMapped=1,603`, 차단 610이다. 이 수치는 scene data·asset·PREPARE·PGM 성공이 아니다. | 집중 테스트, Release 실제 read-only DB 감사와 전체 `REAL_DB_SMOKE: PASS`. FSell/VI PList 행은 0건이지만 운영 production read는 FSell 3×5행, VI 9항목·2 page이고 파일 전후 불변이다. raw 값·인증정보 출력, DB write, Tornado/PGM 명령은 0건이다. 남은 KRX/NXT/live-data·GraphE identity/data·unique mapper 차단을 해소하고 scene/asset/package/PGM gate를 별도로 통과하기 전에는 전체 복원 완료로 올리지 않는다. |
| LF-010 | P1 | 원본 F2/F3 배경 디렉터리 동등성 | 코드 적용 / 외부자산·패키지 검증 대기 | 원본 `MBN_STOCK_N/MainForm.cs:4008-4054`, `btnback_Click`, `ckback_CheckedChanged`; 현재 `MainWindow.Background.cs`, `PlayoutSceneCompositionFactory`, `TrustedPlayoutAssetPath`, `ValidatedPlayoutOptions`; 문서 `docs/TRUSTED_BACKGROUND_ROOT.md` | Cuts gate를 유지한 채 별도 `LegacyBackgroundDirectory`를 추가했고, 생략 시 원본처럼 `SceneDirectory\..\배경`을 계산한다. F2는 유효할 때만 picker 전 `기본.vrv`를 복원하고 F3-on은 항상 기본 파일을 다시 검증한다. root escape·조상/파일 reparse·hardlink·누락·opened-handle final path를 fail closed로 검사하며 Cuts fallback은 없다. 이 장비의 원본 `bin\Debug\배경``기본.vrv`는 실제로 없어 현재 기본 배경은 잠긴다. | Core Debug/Release 1,259/1,259, Playout Debug/Release 387/387과 앱 Debug/Release x64 build를 통과했다. signed 패키지에서 별도 root 상태 메시지와 F2/F3를 확인하고, 자산 제공 뒤 다음 PREPARE mutation/PGM은 승인 회차에서 검증한다. |
| LF-011 | P1 | `종목비교.dat` 기존 저장쌍 이전 | 적용 중 | 원본 `MBN_STOCK_N/Control/UC3.cs:34-55,181-185`, `UC3_Load`, `Data_Load`, `File_Save`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/LegacyComparisonPairImport.cs`, `MainWindow.LegacyComparisonImport.cs`, `Web/comparison-import-workflow.js`, `Web/app.js`; 테스트 `LegacyComparisonPairImportServiceTests.cs`, `comparison-import-workflow.test.cjs`, `comparison-import-app-integration.test.cjs` | 원본 파일은 CP949 358바이트, 비어 있지 않은 8행, 각 행 정확히 9필드이며 0~3열만 값이 있다는 감사 fixture를 SHA-256 `1EE76BC4…BFC0D2F2` 고정했다. Web이 경로를 전달하지 않는 고정 read-only source, reparse/크기/2회 quiescent snapshot 검사, strict CP949/9필드 parser, 현재 Oracle/MariaDB 종목·해외 identity exact read, 고정 시장명과 해외 동명이름 충돌 차단, 전체 중복·500쌍 경계, 명시적 버튼과 one-time marker/rollback을 구현했다. 원본은 수정하지 않고 성공 뒤 WebView private `localStorage` schema만 사용한다. 이 장비의 실제 설정으로 동일 서비스 read-only smoke를 실행해 8행 전부와 SHA-256이 일치하고 모든 identity가 해소됨을 확인했다. 다만 패키지 UI 버튼→localStorage→재시작 marker까지의 1회 실행은 아직 하지 않았으므로 완료로 올리지 않다. | 실제 패키지에서 명시적 버튼 1회로 8행을 모두 fresh DB 재검증하고 local schema/hash/순서를 확인한다. 같은 profile의 두 번째 실행은 marker로 차단되고, 원본 변경·손상·동명이름·기존쌍 중복부분 저장 없이 전체 fail closed여야 한다. |
| LF-012 | P1 | FSell/VI 원본 `Data`의 안전한 이전과 설정 문서 | 적용 중 | 원본 `MBN_STOCK_N/MainForm.cs:338-339`, `FSell.cs:31-88`, `VIList.cs:31-34,137-139`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/LegacyManualOperatorDataImport.cs`, `src/MBN_STOCK_WEBVIEW.Infrastructure/Playout/LegacyManualOperatorDataImporter.cs`, `MainWindow.ManualLists.cs`, `Web/manual-lists-workflow.js`, `Web/manual-lists-ui.js` | UserProfile 아래 고정 원본 경로만 read-only로 두 번 읽고 strict CP949, FSell 파일별 5행+종료 레코드, VI 9열·최대 100건·4,000바이트를 검사한다. 명시적 확인 뒤 private store가 완전히 비어 있을 때만 4개 staged 파일을 no-overwrite로 옮기고 production parser readback 후 v1 marker를 마지막에 기록한다. rollback을 확정하지 못하면 OutcomeUnknown quarantine이며 자동 재시도하지 않는다. 실제 source smoke는 FSell 15행·VI 9건·aggregate SHA-256 `fd39a363…a2114`였고, 이 장비의 private 4개 파일은 이미 원본과 각각 byte-identical이어서 `DestinationNotEmpty`source read 전 중단했으며 전후 SHA가 같았다. | 새 빈 package profile fixture에서 버튼→4파일→marker→재시작 차단을 확인한다. 현재 운영 profile에서는 기존 동일 파일을 덮어쓰거나 marker를 임의 생성하지 않는다. |
| LF-011 | P1 | `종목비교.dat` 기존 저장쌍 이전 | 완료 | 원본 `MBN_STOCK_N/Control/UC3.cs:34-55,181-185`, `UC3_Load`, `Data_Load`, `File_Save`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/LegacyComparisonPairImport.cs`, `MainWindow.LegacyComparisonImport.cs`, `Web/comparison-import-workflow.js`, `Web/app.js`; 테스트 `LegacyComparisonPairImportServiceTests.cs`, `comparison-import-workflow.test.cjs`, `comparison-import-app-integration.test.cjs`; 실행 증거 `docs/LEGACY_ONE_TIME_IMPORT_AUDIT.md` | 원본 CP949 358바이트·8행·9필드와 SHA-256 `1EE76BC4…BFC0D2F2` 고정하고, 고정 read-only source·strict parser·현재 Oracle/MariaDB exact identity·전체 원자 저장·one-time marker를 적용했다. 설치된 signed 1.0.3 package에서 빈 상태 `0 PAIRS`를 확인한 뒤 명시적 버튼을 정확히 한 번 실행해 8쌍을 저장했다. 정상 종료·재시작 후에도 `8 PAIRS`, 원본 순서의 8행과 완료형 `원본 8쌍 가져옴` gate가 유지됐고, 원본 파일 SHA-256은 불변이었다. 원본·DB·Tornado에는 쓰기나 명령을 보내지 않다. | Core 20/20, Web 12/12, 실제 read-only DB identity 8/8, package 0→8 및 재시작 marker 영속성 PASS. 손상·동명이름·기존쌍 중복·부분 실패는 계속 전체 fail closed이며 package family data를 초기화해 같은 회차를 재현하지 않는다. |
| LF-012 | P1 | FSell/VI 원본 `Data`의 안전한 이전과 설정 문서 | 적용 중 | 원본 `MBN_STOCK_N/MainForm.cs:338-339`, `FSell.cs:31-88`, `VIList.cs:31-34,137-139`; importer `LegacyManualOperatorDataImport`, `LegacyManualOperatorDataImporter`; production read `S5025TrustedManualFileDataSource`, `ViTrustedManualFileStore`; 감사 `tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedManualRestoreReadOnlyAudit.cs`, `docs/NAMED_MANUAL_RESTORE_READ_ONLY_AUDIT.md` | 고정 원본 경로·strict CP949·source/hash 재검증·no-overwrite staged commit·marker-last·OutcomeUnknown quarantine를 유지한다. 격리 fixture는 FSell 15행·VI 9건, aggregate SHA-256 `fd39a363…a2114`, 첫 실행 성공과 두 번째 `AlreadyImported`를 확인했다. 운영 profile은 이미 원본과 byte-identical한 4파일이 있어 importer가 `DestinationNotEmpty`차단되며 marker/intent를 만들지 않는다. production opened-handle read는 FSell 3 audience 각각 5행, VI 9항목·2 page와 안정된 두 번째 version을 확인했고 감사 전후 4파일은 불변이다. 현재 PList에는 FSell/VI 행이 0건이다. | 새 빈 signed package profile의 명시적 UI importer 검증은 남아 있다. 현재 운영 profile 동일 파일을 덮어쓰거나 marker를 임의 생성하지 않는다. PList에 향후 FSell/VI 행이 생기면 같은 fresh source/version gate를 거치며 scene data·asset·PREPARE·PGM은 별도 검증한다. |
| LF-013 | P1 | generic native `bridge-error` 표시 | 완료 | producer `MainWindow.xaml.cs`, `MainWindow.Playout.cs`; consumer `Web/app.js` `handleBridgeError`/`handleNativeMessage`; 테스트 `operator-ui-app-integration.test.cjs` | bounded toast/log로 소비하며 malformed status/quarantine payload에서 playout state를 변경하지 않는다. 전체 Web 371/371과 signed 1.0.3 패키지 반영을 확인했다. | 새 native error producer를 추가할 때마다 같은 bounded consumer 계약을 유지한다. |
| LF-014 | P2 | 영구 운영 로그 | 완료 | 원본 `MBN_STOCK_N/MainForm.cs:84-180`; 현재 `NativeOperatorLogWriter.cs`, `NativeOperatorLogTransitions.cs`, `MainWindow.NativeOperatorLog.cs` | `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Logs\Log_MMdd.log`에 폐쇄 event/state만 기록하며 4MiB/31일, 중복 억제, reparse/handle 검증, single terminal/OutcomeUnknown 규칙을 적용한다. signed 1.0.3 package context에서 Startup→DryRunReady→Oracle/MariaDB Healthy→Shutdown→재시작이 같은 파일에 잔존했고 SQL·경로·인증·requestId 금지 문자열은 0건이었다. | 운영 회차에서도 로그를 명령 재시도 근거로 사용하지 않고 authoritative 출력과 함께 판독한다. |
| LF-015 | P2 | 호출자가 없는 native bridge handler | 완료 | `MainWindow.xaml.cs`, `OnWebMessageReceived`; 테스트 `tests/Web/operator-ui-app-integration.test.cjs` | Web 호출자가 없던 `open-external``reload` message case를 native allowlist에서 제거했다. 외부 navigation/new-window 처리와 WebView process failure의 내부 reload 복구 메서드는 별도 native event 경로로 유지한다. | 정적 계약 테스트에서 두 message case와 Web 발신 literal이 모두 없음을 확인했다. |
| LF-016 | P2 | 실제 preview 이미지·영상 | 외부자산 필요 | `docs/MIGRATION.md:73-74`; 원본 `MBN_STOCK_N/MainForm.cs:1448-1462,1894-1911,2919-2921` | 현재 preview는 안전한 DTO/mutation 텍스트 중심이다. 실제 이미지·영상 연결은 후속 자산이다. 다만 원본 thumbnail 호출과 watcher는 주석 또는 미호출이므로 활성 원본 동등성의 P0/P1 blocker로 보지 않는다. | 승인된 preview 자산이 생기면 asset root와 노출 정보 검사를 거쳐 패키지 화면에서 확인한다. |
| LF-017 | P1 | 모든 업무 탭에서 계속 보이는 종목 검색·31컷 | 적용 중 | 원본 `MBN_STOCK_N/MainForm.Designer.cs:2022-2036,2454-2462`; 현재 `Web/index.html`의 독립 `stock-panel`, `Web/app.js``renderStockWorkflow`; 테스트 `operator-ui-app-integration.test.cjs` | 종목 검색·31컷을 catalog panel 밖의 독립 열로 옮기고 모든 업무 메뉴에서 항상 표시한다. 1920 브라우저에서 11개 메뉴를 이동해 검색어, 표시, 31개 행이 유지되고 문서 가로 overflow가 없음을 확인했다. | signed MSIX에서 실제 DB 검색 결과·선택 종목이 모든 탭 전환 뒤 유지되고 컷 추가가 같은 identity를 쓰는지 확인한 뒤 완료로 올린다. |
| LF-018 | P2 | 원본 트리·컷 목록 일괄 추가 제스처 | 적용 중 | 원본 부모 double-click `MBN_STOCK_N/Control/UC1.cs:141-180`; 소스 컷 Ctrl/Shift 선택 `MBN_STOCK_N/MainForm.cs:3427-3525,3534-3549,3635-3643`; 현재 `Web/legacy-batch-selection-workflow.js`, `Web/app.js`; 테스트 `legacy-batch-selection-*.test.cjs` | 종목 31컷의 plain/Ctrl/Shift/Ctrl+Shift 선택, 단일 double-click, `선택 컷 추가`를 포팅했다. 해외·환율·지수 fixed source의 섹션 `전체 추가`는 모든 자식을 먼저 materialize한 뒤 원본 순서로 한 번에 commit하며, 수동/지원불가가 한 건이라도 있으면 부분 추가 없이 전체 차단한다. 실제 브라우저에서 5개 선택, 해외 43개 순서 추가, 수동 혼합 섹션 0건 추가를 확인했다. | signed MSIX 확인 뒤 업종·비교·테마 등 전용 source group의 부모 전체 추가가 원본에서 실제로 필요한 범위를 계속 분리 포팅한다. source drag/drop 자체는 오조작 검증 뒤 결정한다. |
| LF-019 | P1 | 저장 PList의 `테마_NXT` stale code 복원 | 적용 중 | 원본 `Control/UC4.cs`, `Scene/s5074.cs`, `s5077.cs`, `s5088.cs`; 현재 `LegacyNamedNxtThemeRestoreService`, `MainWindow.NamedNxtThemeRestore.cs`, `Web/named-nxt-theme-restore-workflow.js`; 상세 `docs/NAMED_NXT_THEME_RESTORE.md` | 원본처럼 제목 exact lookup으로 현재 Maria code를 찾고 현재가 전용 5/6/12행, 정렬 fallback, 00~12 PRE/13~23 AFTER, fresh PageN을 닫힌 batch로 복원한다. 실제 활성 48행/24제목은 모두 current identity가 유일하고 저장 code 48개가 전부 stale였지만 현재 preview가 전부 비어 있어 48행 모두 `PreviewEmpty` 정상 차단다. KRX 테마는 후보가 아니다. | Maria `SB_ITEM`/`v_all_stock` live 연계가 복구된 뒤 같은 read-only audit에서 한 행 이상 live item을 확인하고, fresh page preflight와 Debug/Release/MSIX UI를 검증한다. 실제 PGM은 별도 승인 회차가 필요하다. |
| LF-019 | P1 | 저장 PList의 `테마_NXT` stale code 복원 | 코드 완료 / DB view 정합성 대기 | 원본 `Control/UC4.cs`, `Scene/s5074.cs`, `s5077.cs`, `s5088.cs`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/LegacyNamedNxtThemeRestore.cs`, `MainWindow.NamedNxtThemeRestore.cs`, `Web/named-nxt-theme-restore-workflow.js`; 테스트 `LegacyNamedNxtThemeRestoreServiceTests.cs`, `named-nxt-theme-restore-workflow.test.cjs`, `named-nxt-theme-restore-app-integration.test.cjs`; 상세 `docs/NAMED_NXT_THEME_RESTORE.md`, `docs/NXT_THEME_RESTORE_READ_ONLY_AUDIT.md` | 제목 exact lookup, 현재 Maria code, 5/6/12행, 정렬 fallback, 00~12 PRE/13~23 AFTER fresh PageN을 닫힌 batch로 복원한다. 실제 48행/24제목은 current identity가 유일하고 저장 code 전부 stale다. `SB_ITEM` 446행과 NXT master 446행은 존재하지만 원본과 새 앱이 공통으로 쓰는 `v_all_stock` 조인은 0행이어서 48행 모두 `PreviewEmpty`로 차단다. 통합 PList 결과에서도 `selectionMapped`로 계산하지 않았고 추측성 직접-table fallback을 적용하지 않았다. | Core Debug/Release와 실제 read-only DB smoke PASS, DB write·Tornado/PGM 명령 0건. DBA가 `v_all_stock`의 NXT 포함 의도를 확인해 승인된 변경·rollback으로 복구하거나 직접-table 경로를 별도 승인한 뒤 fresh page preflight/MSIX/PGM을 검증한다. |
| LF-020 | P2 | 원본 초기 업무 화면 `해외` | 완료 | 원본 `MainForm.Designer.cs` 첫 TabPage `해외`; 현재 `Web/index.html`, `Web/app.js`; 테스트 `operator-ui-app-integration.test.cjs` | dashboard는 진단 화면으로 유지하되 초기 active identity/제목/요청을 해외로 맞춰다. 실제 브라우저에서 해외만 active, 최초 요청 1회, dashboard 진입·복귀를 확인했고 signed 1.0.3에 같은 Web 31개가 byte 일치했다. | 후속 탭 추가 시에도 초기 identity를 순번이 아닌 `data-market`으로 유지한다. |
| LF-021 | P2 | 업무 탭 드래그 순서 변경 | 완료 | 원본 `MainForm.cs:3183-3312`; 현재 `Web/market-nav-reorder.js`, `Web/app.js`; 테스트 `market-nav-reorder*.test.cjs` | dashboard는 첫 위치에 고정하고 10개 업무 identity만 drag 교환/Alt+위·아래로 이동한다. active view/DB/playlist는 불변이고 입력·modal·PREPARE 잠금에서 fail closed하며 세션 종료 시 기본 순서로 돌아간다. 실제 브라우저에서 `Alt+Down` 후 active 해외 유지와 reload 초기화를 확인했다. | signed 1.0.3 패키지에 Web 31개 일치로 반영했다. |
| LF-022 | P2 | GraphE 위/아래 찾기 | 완료 | 원본 `GraphE.cs:338-379,605-611`; 현재 `Web/manual-financial-ui.js/.css`; 테스트 `manual-financial-ui.test.cjs` | `위로 찾기`/`아래로 찾기` 버튼과 Enter 다음 찾기를 포팅했다. trim·대소문자 무시 Contains, 현재 행 전/후, no-wrap, 모든 pending·잠금·hidden 차단을 유지한다. 원본의 `i > 0` 첫 행 누락은 명백한 off-by-one으로 보고 첫 행도 정상 후보에 포함했다. | GraphE 집중 20/20, 전체 Web 371/371과 signed 1.0.3 Web 일치 통과. |
@@ -47,26 +47,33 @@
| LF-024 | P2 | GraphE 종목명 헤더 정렬 | 완료 | 원본 `GraphE.cs:258-280`; 현재 `Web/manual-financial-ui.js/.css`; 테스트 `manual-financial-ui.test.cjs` | `종목명 ↕` 첫 click 내림차순/둘째 click 오름차순을 세션 로컬 display order로 적용했다. 선택 snapshot/native/playlist는 불변이고 위/아래 찾기는 현재 표시 순서를 따른다. 새 화면/목록에서 오름차순으로 초기화한다. | 네 모드·한글/ASCII·빈/단일·pending·정렬된 find 집중 20/20, 전체 Web 371/371 통과. |
| LF-025 | P2 | UC7 거래정지 종목명 헤더 정렬 | 완료 | 원본 `Control/UC7.cs:173-188`; 현재 `Web/trading-halt-workflow.js`, `Web/app.js`, `Web/index.html`; 테스트 `trading-halt-name-sort-app-integration.test.cjs` | `종목명 ↕` 첫 click 내림차순/둘째 click 오름차순을 표시 order에만 적용했다. 동명은 market+stockCode로 결정적으로 정렬하고 선택 identity를 유지하며 새 검색은 server 순서로 초기화한다. DB/native/playlist 변경은 없다. | 집중 18/18, 전체 Web 371/371, 실제 브라우저에서 접근 라벨/가로 overflow 0, signed 1.0.3 Web 일치 통과. |
## 활성 PLAY_LIST 2,213행 재분류
## 활성 PLAY_LIST 2,213행 read-only 재분류
운영 Oracle의 활성 프로그램 정의 23개와 연결된 2,213행을 원문을 출력하지 않는 read-only 집계로 다시 분류했다. 삭제된 정의에 매달린 orphan 데이터 88개 프로그램/9,369행은 현재 PList의 정상 load 대상이 아니므로 아래 수치에 포함하지 않았다.
운영 Oracle의 활성 프로그램 정의 23개와 연결된 2,213행을 원문을 출력하지 않는
감사로 다시 분류했다. 원본 `PList.data_load`의 page 공백 제거를 제외하면 모든
필드를 그대로 보존했고, 현재 persistence 결과와 2,213/2,213 일치했다.
| 복원 분류 | 건수 | 판정 |
| route | 건수 | 현재 결과 |
|---|---:|---|
| 기존 닫힌 매퍼와 fresh 검증으로 복원 | 1,573 | stock/fixed/expert/GraphE 등 기존 계약과 현재 master를 모두 통과하는 행 |
| DB 없이 추가 복원 가능 | 57 | 업종 fixed 24, 고정 시장·지수 2열판 33 |
| 현재 DB exact lookup 뒤 추가 복원 가능 | 228 | NXT theme 48(복원 경로 적용, 현재 preview empty로 전부 차단), 비교 graph 41, 해외종목 78, 해외지수 candle 61 |
| 현재 identity를 안전하게 증명할 수 없음 | 325 | KRX theme 300, 원본이 시장을 저장하지 않은 종목 2열판 25. 이름이나 과거 코드를 추정하지 않고 재선택이 필요함 |
| whitelist 불일치로 자동 복원 금지 | 30 | `s5001` fixed-nearest 기존 집계가 재감사 후보 40건과 달라 명시적 CASE 확정 전까지 차단 |
| 합계 | 2,213 | raw 행은 closed mapping과 fresh identity가 모두 성공해야만 playable entry가 됨 |
| 기존 닫힌 매퍼 | 1,250 | hash 고정 원본 INI와 현재 exact signature로 selection 1,250개 매핑 |
| 업종 fixed 닫힌 매퍼 | 24 | selection 24개 매핑 |
| 비교 fresh identity | 99 | selection 74개 매핑, 시장 identity 유실 25개 차단 |
| 해외지수 candle fresh identity | 61 | selection 61개 매핑 |
| NXT theme fresh identity | 48 | validated preview empty 48 차단 |
| KRX theme fresh identity | 587 | current-code remap 94, identity 없음 271, 저장 행 무효 16, preview empty 193, 지원 불가 action 13 |
| 해외종목 fresh identity | 78 | exact 입력명에서 unique symbol/nation을 correlated materialize해 78개 매핑 |
| GraphE fresh materialization | 36 | 별도 double-read row-version·stock identity·cut 계약으로 22개 매핑, 저장값 무효 9·identity 중복 5 차단. FSell/VI PList행은 0 |
| unique closed mapper 없음 | 30 | 원본 328-action runtime hash와 현재 exact signature 중 단일 후보가 없어 nearest scene 추정 없이 차단 |
| 합계 | 2,213 | 기본 감사 `selectionMapped=1,581`/차단 632; 별도 GraphE proof를 합치면 `selectionMapped=1,603`/차단 610 |
테마 635행은 별도로 다음과 같이 판정했다. KRX exact 문법 375행 중 현재 제목이 유일한 287행만 즉시 신뢰할 수 있고 88행은 stale이다. 정렬 토큰이 없는 199행과 기타 문법 13행은 원본 `s5074/s5077/s5088`의 최종 분기와 같이 모두 `GAIN_ASC` 의미지만, 현재 `SB_LIST`에서 제목이 사라져 playable하지 않다. NXT 48행은 24개 저장 제목으로 구성되고 raw grammar는 `5단 표그래프 / 테마-현재가(입력순)` 한 종류다. 48행 모두 제목의 현재 Maria identity는 유일하고 저장 code가 현재 code와 다르지만, 현재 validated preview가 0건이어서 전부 `PreviewEmpty`로 차단한다. live item이 생긴 경우에만 현재 code를 송출 selection에 사용하며, 원본과 같이 PREPARE 직전 로컬 시각 00~12시는 PRE, 13~23시는 AFTER로 다시 결정한다.
비교 후보 99행은 고정 지수쌍 33, 시장쌍이 보존된 KRX graph 41, 시장이 사라진 `종목` 2열판 25로 확정했다. 앞의 74행만 closed target 또는 Oracle의 시장+이름 exact lookup으로 복원한다. 마지막 25행은 오늘의 master에서 이름이 우연히 유일해도 원본 행이 KOSPI/KOSDAQ/NXT/해외 구분을 보존하지 않았으므로 자동 복원하지 않는다. NXT graph는 원본 UC1 자체가 지원하지 않았고 현재 graph loader도 Oracle KRX pair만 허용하므로 계속 차단한다.
해외지수 candle 61행은 UC5 고정 이름 세 개(Dow 22, Nasdaq 21, S&P500 18)와 5/20/60/120일 기간만 사용하며 240일 활성 행은 없다. 이름과 기간은 닫힌 표이지만 원본 LIST_TEXT가 symbol/nation을 저장하지 않으므로 DB-free로 분류하지 않는다. Oracle master에서 `F_INPUT_NAME`, expected symbol, `F_NATC=US`, `F_FDTC=0`의 exact identity를 다시 확인한 뒤에만 `s8010/PRICE` 선택을 발급한다.
이 수치는 “문자열을 해석할 수 있음”과 “현재 데이터로 송출할 수 있음”을 분리한다. identity 미확정 325행과 whitelist 미확정 30행에는 `R...` 임시 code나 가장 가까운 장면을 넣지 않는다. 화면에는 실패 행을 유지하고 PREPARE를 차단하며, 운영자가 현재 항목으로 다시 선택해야 한다.
`unique=2,213`은 전부 송출 가능하다는 뜻이 아니라 모든 행이 단일 route의
selection 결과 또는 명시적 차단 사유를 가졌다는 뜻이다. `selectionMapped`
장면 데이터·자산, package context, PREPARE 또는 PGM 성공이 아니다. 이들은 집계
밖의 별도 gate다. KRX/NXT theme은
원본 `s5074/s5077/s5088`처럼 제목 exact lookup을 사용하며, 저장 code를 가장 가까운
현재 code로 추정하지 않는다. 비교 25행도 현재 이름이 우연히 유일하더라도 저장
행이 시장을 보존하지 않았으므로 계속 차단한다. 상세 SQL 경계와 결과는
[`NAMED_PLAYLIST_READ_AUDIT.md`](NAMED_PLAYLIST_READ_AUDIT.md)에 기록했다.
## s6001 asset 검사 보강 기준

View File

@@ -0,0 +1,136 @@
# 원본 one-time importer 안전 감사
기준일은 2026-07-12이며, 설치된 패키지는
`Wickedness.MBNStockWebView_1.0.3.0_x64__qbv3jkvsn3aj0`이다. 이 감사에서는
원본 `Data`를 읽기 전용으로 사용했고 Oracle/MariaDB에는 SELECT만 실행했다.
운영 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\OperatorData`와 Tornado2에는 쓰기나
명령을 보내지 않았다. 비교쌍에 한해서는 설치된 signed package의 명시적
버튼을 한 번 눌러 WebView2 `localStorage`에 8쌍과 one-time marker를 같은
envelope로 저장했다.
## 감사 시점 운영 profile 분류
FSell/VI 저장소에는 marker와 intent 없이 원본과 byte 단위로 같은 네 파일이
이미 있다.
| 파일 | 길이 | SHA-256 | 고정 원본과 일치 |
|---|---:|---|---|
| `개인.dat` | 56 | `F953366A5D9F940CBE54F05C256F8BFB0AF4674C8F4D46CCBF839A63113342FA` | 예 |
| `외국인.dat` | 58 | `45F62038E3216E80FCF22F4E923056858E9C3537BF0DC46BC359148E82DD7CD8` | 예 |
| `기관.dat` | 27 | `BC4D62D734E701744E10DD830A2D6B9AEBEBEB795BB4DABC77CDE6577F016EB7` | 예 |
| `VI발동.dat` | 276 | `46AFBA07D2C4AF50717450582DEB10EC10367701D09161BECF5E32342AD4AA45` | 예 |
- `.legacy-manual-operator-import.v1.json`: 없음
- `.legacy-manual-operator-import.v1.in-progress.json`: 없음
- importer staging 파일: 없음
- startup 상태: marker가 없는 기존 원본 파일을 그대로 쓰는 `ReadyLegacyData`
- 현재 버튼 실행 판정: `DestinationNotEmpty`, `OutcomeUnknown=false`
따라서 현재 profile에서 `원본 데이터 가져오기`를 누르면 네 파일을 덮어쓰지
않고 source read 전에 전체 import가 중단된다. marker를 임의로 만들어
`AlreadyImported`로 바꾸거나 파일을 지워 import를 재현하면 안 된다.
다음 명령은 운영 profile을 쓰지 않고 이 판정을 다시 출력한다.
```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj `
-c Release -p:Platform=x64 -- --manual-import-audit
```
현재 결과:
```text
MANUAL_IMPORT_AUDIT: PASS sourceFiles=4/4 operatingFiles=4/4 sourceByteIdentical=true marker=false intent=false staged=false runtime=ReadyLegacyData decision=DestinationNotEmpty outcomeUnknown=false writesAttempted=false
```
## 격리 FSell/VI 실제 원본 import
`--manual-import-fixture`는 사용자가 destination을 지정할 수 없게 닫혀 있다.
고정 원본을 읽고 `%TEMP%\MBN_STOCK_WEBVIEW-manual-import-fixture-<GUID>`에만
쓴다. 전체 실행은 1분 timeout으로 제한되며 timeout/cancel 뒤 자동 재시도하지
않는다. 다음 항목을 모두 확인한 뒤 자신이 만든 prefix의 non-reparse directory만
삭제한다.
1. 4파일 stage 및 no-overwrite commit
2. production parser read-back
3. marker-last commit과 intent 제거
4. 재시작 guard의 `ReadyImportedData`
5. 같은 fixture의 두 번째 실행 `AlreadyImported`, `OutcomeUnknown=false`
6. 원본 4파일과 운영 import 대상 6항목의 실행 전후 SHA-256 불변
7. 원본 aggregate SHA-256 `fd39a363…a2114`, FSell 15행과 VI 9건 고정
```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj `
-c Release -p:Platform=x64 -- --manual-import-fixture
```
실제 결과:
```text
MANUAL_IMPORT_FIXTURE: PASS rows=15 vi=9 sourceSha256=fd39a3639e574a19f174225e8e65234d6bdd4c58700bb5d10230a779a03a2114 state=ReadyImportedData secondAttempt=AlreadyImported outcomeUnknown=false sourceUnchanged=true operatingImportSetUnchanged=true
```
## `종목비교.dat` 분류
- 고정 원본: 358바이트, 8행
- SHA-256: `1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2`
- 실제 Oracle/MariaDB read-only 재검증: 8/8 identity 해소
- 실행 경로: `비교` 화면의 `원본 비교쌍 가져오기`
- 성공 저장 위치: package WebView2 `localStorage`의 단일 comparison envelope
감사 초기에 package WebView2가 닫힌 상태에서 LevelDB에는 87바이트
`VERSION=1` log 외 comparison key byte가 없었고 비교 화면은 `0 PAIRS`였다.
signed 1.0.3 패키지에서 `원본 비교쌍 가져오기`를 명시적으로 한 번 실행한
결과, native가 고정 원본의 SHA-256과 8행을 다시 읽고 현재 DB에서 모든
identity를 해소한 뒤 화면이 `8 PAIRS``원본 8쌍 가져옴`으로 바뀌었다.
부분 저장이나 `복구 필요` 상태는 발생하지 않았다.
이 성공은 같은 package profile의 단일 comparison envelope에 pair list와
marker를 함께 기록한 결과다. 앱을 정상 종료한 뒤 설치된 signed 1.0.3을
다시 실행해 다음 상태를 확인했다.
- 비교 화면: `8 PAIRS`
- 저장 비교쌍: 원본 순서의 8개 항목 유지
- one-time gate: 완료형 `원본 8쌍 가져옴`, 다시 실행할 수 없는 상태
- 고정 원본 SHA-256: `1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2`로 불변
따라서 package UI 저장과 재시작 marker 영속성은 PASS다. 실행 중 LevelDB는
process lock 상태이므로 raw 파일 검색으로 live logical state를 단정하거나
파일을 복사·수정하지 않았다.
정규 DB smoke에는 실제 importer를 추가했다. 아래 명령은 원본을 읽고 8개
identity를 fresh SELECT로 해소하지만 package `localStorage`에는 쓰지 않는다.
```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj `
-c Release -p:Platform=x64 --no-build
```
확인 결과는 다음과 같다.
```text
LegacyComparisonImport: rows=8, sha256=1EE76BC464FB1C44C8B6546E99CDC21C726C0E2C24AD344F5C19AE41BFC0D2F2, identities=fresh-read-only
REAL_DB_SMOKE: PASS
```
현재 package profile에서는 실제 0→8 import와 정상 종료 후 재시작 marker
검증을 완료했으므로 이를 재현하려고 package family 데이터를 초기화하지
않는다. 빈 profile의 실패 fixture가 추가로 필요하면 별도 test package
identity를 사용한다. 운영 identity에서 `Remove-AppxPackage`, app data reset,
LevelDB 파일 복사·삭제로 빈 profile을 만들지 않는다.
## 차단 판정표
| 상태 | FSell/VI 판정 | 비교쌍 판정 | 자동 재시도 |
|---|---|---|---|
| 대상 파일 없음, source 정상 | import 가능 | marker 없고 envelope 정상일 때 가능 | 명시적 버튼만 |
| 기존 4파일, marker 없음 | `DestinationNotEmpty` | 해당 없음 | 금지 |
| 유효 marker 있음 | `AlreadyImported` | 버튼이 `원본 N쌍 가져옴`으로 비활성 | 금지 |
| intent 있음 | `InterruptedImport`, `OutcomeUnknown=true` | 해당 없음 | 금지, 수동 대조 |
| marker/4파일 불일치 | `InvalidImportedState` | 해당 없음 | 금지, 수동 대조 |
| split/손상 envelope | 해당 없음 | `복구 필요`, 전체 차단 | 금지, local state 대조 |
| DB timeout/identity 오류 | 해당 없음 | 저장 전 전체 취소 | 금지 |
FSell/VI 전용 화면은 원본 운영 항목의 개인·외국인·기관 또는 VI 수동 항목을
열어 진입하며, header의 `원본 데이터 가져오기`가 importer 버튼이다. 비교쌍은
상단 `비교` 업무 탭 안의 `원본 비교쌍 가져오기` 버튼으로 진입한다.

View File

@@ -70,8 +70,8 @@ WebView는 `https://app.mbn.local` 가상 호스트로 패키지 내부 파일
- 화면/계약 구현: MainForm과 UC1~UC7의 종목·고정·업종·비교·테마·해외·전문가·거래정지 workflow, GraphE·FSell·VIList·PList/AList·ThemeA·EList 전용 화면과 native bridge
- 자동 계약 검증: 328 fixed + 44 industry + 31 stock + 9 comparison의 412 action 매트릭스, typed DTO, 저장·복원 fail-closed 경계
- 적용 중: PList의 기존 한국어 `DC_TITLE`/`LIST_TEXT` 복원. 기존 운영 DB 행을 원본과 문자 단위로 대조하기 전에는 완료로 판정하지 않
- 화면/계약 구현·운영 검증 대기: GraphE·FSell·VI 항목을 포함한 manual named save→reload→fresh identity/version/page plan 복원, ThemeA/EList의 KRX/NXT·동명이름·코드 identity 보존
- 적용 중: PList의 기존 한국어 `DC_TITLE`/`LIST_TEXT` 2,213행은 원문 round-trip과 typed persistence가 전부 일치했다. KRX·해외종목 및 별도 GraphE fresh proof를 합쳐 `selectionMapped=1,603`, 차단 610이며 이 수치는 scene data·asset·PREPARE·PGM 성공을 뜻하지 않는다.
- 화면/계약 구현·운영 검증 대기: GraphE 36행 중 22행은 fresh row-version/stock identity로 복원되고 14행은 저장값 무효·identity 중복으로 차단된다. FSell/VI PList행은 현재 0건이며 production source read만 FSell 3×5행, VI 9항목·2 page로 확인했다. ThemeA/EList 운영 write와 PGM은 별도 승인 범위다.
- 코드 적용·운영 검증 대기: 원본 `Data\종목비교.dat`, FSell, VI를 고정 read-only source에서 strict parser·hash·fresh identity·durable intent로 검증한 뒤 private 저장소로 이전하는 one-time importer. 실제 package profile의 명시적 1회 실행은 아직 수행하지 않음
- 외부자산 필요: `s5006``Video\큐브배경.vrv`, `s6001` 해외지수의 국가별 영상 13개. 현재 alias-only cut 검사를 asset-ready 근거로 사용하지 않음
- 운영 동등성 미검증: GraphE, PList/AList, ThemeA, EList의 실제 Oracle DB-W와 FSell/VI 운영 파일 write/read-back

View File

@@ -0,0 +1,118 @@
# 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
승인 회차에서만 수행한다.

View File

@@ -0,0 +1,178 @@
# PList 기존 데이터 읽기 전용 동등성 감사
기준일: 2026-07-12
원본(읽기 전용): `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)
## 원본 읽기 의미
원본 `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`, 종목명, 테마명, 프로그램
코드 또는 인증정보를 포함하지 않는다.
## 실제 Oracle/MariaDB 결과
실제 운영 연결에 고정 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필드 규칙으로 변환했다. 원본 328 action에서
현재 명시적으로 unavailable인 수동 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를 만들거나 파일을 쓰지 않았다.
## 구현과 자동 근거
- 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`로 끝났다. DB write와 Tornado2/PGM 명령은 0건이며 raw 값이나
인증정보도 출력하지 않았다.

View File

@@ -0,0 +1,72 @@
# NXT 테마 복원 read-only 진단
검증일: 2026-07-12 (KST)
## 결론
활성 NXT 테마 플레이리스트 48행(현재 고유 테마 24개)이 `PreviewEmpty`
차단되는 원인은 마이그레이션의 테마 제목/코드 선택이나 DB endpoint 차이가
아니다. 현재 MariaDB의 `SB_ITEM`에는 해당 테마 구성종목 446행이 있고 그
446행 모두 폐쇄된 X/T 종목코드 형식을 만족하지만, 원본 장면이 사용하는
`v_all_stock` 조인에는 단 한 행도 결합되지 않는다.
같은 prefix 제거 규칙으로 기반 NXT 테이블을 확인하면 다음과 같다.
| read-only 집계 | 행 수 |
|---|---:|
| current theme `SB_ITEM` | 446 |
| `N_STOCK` 결합 | 187 |
| `N_KOSDAQ_STOCK` 결합 | 259 |
| 두 master 결합 합계 | 446 |
| `N_ONLINE` 결합 | 151 |
| `N_KOSDAQ_ONLINE` 결합 | 169 |
| 원본 `v_all_stock` 결합 | 0 |
| non-stop / live 결합 | 0 / 0 |
즉 구성종목은 두 NXT master에 전부 존재하고 그중 320행은 online 테이블에도
존재한다. 현재 `v_all_stock` 정의/데이터 범위가 이 NXT master 행을 포함하지
않는 것이 직접 원인이다.
## 원본 동등성 확인
읽기 전용으로 조사한 원본 `s5074`, `s5077`, `s5088`은 모두 NXT 구성종목을
다음 조건으로 읽는다.
```sql
SUBSTRING(SB_ITEM.SB_I_CODE, 2, 12) = v_all_stock.F_STOCK_CODE
```
`LegacyThemeSelectionService`
`LegacyNamedNxtThemeRestoreService`도 같은 prefix 제거 규칙, 같은
`F_STOP_GUBUN = 'N'` 조건을 사용한다. 원본 설정과 현재 runtime 설정의
host/port/database를 비밀값 없이 정규화해 비교한 결과 SHA-256과 값이
동일했고, 실제 `DATABASE()`도 현재 runtime database와 일치했다. 서버는
MariaDB 10.5 계열이다.
따라서 다른 DB를 바라보는 마이그레이션 오류가 아니며, 현 데이터 상태에서는
원본 프로그램도 같은 `v_all_stock` 경로로 NXT 테마 구성종목을 얻지 못한다.
## 안전 결정
- 운영 DB write는 수행하지 않았다.
- endpoint, 계정, 비밀번호, 테마/종목 제목과 코드는 출력하거나 기록하지 않았다.
- exact, 양쪽 prefix 제거, right-six 후보도 모두 0이어서 코드 형식 추측
fallback의 근거가 없다.
- `N_STOCK`/`N_KOSDAQ_STOCK` 직접 fallback은 데이터를 찾지만 원본 SQL과 다른
동작이므로 자동 적용하지 않았다.
복구는 DBA가 `v_all_stock`의 NXT 포함 의도를 확인하고 승인된 변경/롤백
계획으로 view를 교정하거나, 제품 요구사항으로 원본과 다른 NXT master 직접
경로를 명시적으로 승인한 뒤 진행해야 한다. 그 전까지 복원은 `PreviewEmpty`
fail-closed 상태를 유지한다.
## 재현
```powershell
dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj `
-c Release --no-restore
```
스모크는 식별자를 출력하지 않고 endpoint equality, aggregate join 수,
분류(`VAllStockExcludesCurrentNxtMasterRows`)만 출력한다. 전체 결과는
`REAL_DB_SMOKE: PASS`여야 한다.

View File

@@ -218,11 +218,11 @@
| `PList_Load`, `Get_FileName_Folder` | `DC_LIST`의 프로그램 제목·코드를 조회한다. | native `request-named-playlist-list`; `named-playlist-workflow.js` | 화면 통합 완료 |
| `AList_Load`, `GetNextCode`, `btnok_Click` | 다음 8자리 코드와 프로그램명을 생성해 `DC_LIST`에 추가한다. | native `create-named-playlist` | 화면 통합 완료. parameterized transaction 사용. |
| `PList.data_save`, double-click/확인 | 기존 `PLAY_LIST`를 지운 뒤 enabled와 6개 표시 필드를 `LIST_TEXT` 및 ITEM_INDEX 순서로 저장한다. | native `replace-named-playlist`; trusted entry serialization | 화면/계약 구현. raw·untrusted entry는 저장 또는 PREPARE 불가. GraphE·FSell·VI 등 manual 항목의 실제 save→reload→fresh identity/version 보존은 미검증. |
| `PList.data_load`, double-click/확인 | 프로그램 코드별 `PLAY_LIST`를 ITEM_INDEX 순으로 읽어 playlist를 교체한다. | native `request-named-playlist-load`; resolver 기반 materialization | 화면/계약 구현. raw DB row는 builder/selection/page를 재검증할 때까지 PREPARE 불가. 기존 한국어 `DC_TITLE`/7필드 `LIST_TEXT` 무손상 복원은 적용 중. |
| `PList.data_load`, double-click/확인 | 프로그램 코드별 `PLAY_LIST`를 ITEM_INDEX 순으로 읽어 playlist를 교체한다. | native `request-named-playlist-load`; resolver 기반 materialization | 화면/계약 구현. 실제 2,213행의 한국어 `DC_TITLE`/7필드 `LIST_TEXT` round-trip과 typed persistence는 전부 일치했다. KRX·해외종목·GraphE를 포함한 통합 read-only 증거는 `selectionMapped=1,603`, 차단 610이며 raw DB row는 scene data/asset/page를 재검증할 때까지 PREPARE 불가다. |
| `PList.button1_Click` | 프로그램 정의 삭제 | native `delete-named-playlist` | 화면 통합 완료. 실제 운영 DB-W 없음. |
| 취소 | 아무 변경 없이 닫는다. | mutation 요청 없음 | 화면 통합 완료. |
구현은 `src/MBN_STOCK_WEBVIEW.Core/Data/NamedPlaylistPersistence.cs`, `src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OracleNamedPlaylistMutationExecutor.cs`, `MainWindow.NamedPlaylists.cs`, `Web/named-playlist-workflow.js`에 있다. 자동 근거는 named playlist Core/Infrastructure/Web/bridge 테스트다. 실제 운영 DB write, 기존 한국어 데이터의 원본 대조, manual named save의 fresh restore 및 named playlist 전용 PGM 검증은 아직 없다.
구현은 `src/MBN_STOCK_WEBVIEW.Core/Data/NamedPlaylistPersistence.cs`, KRX/NXT/해외종목/manual 전용 restore, `src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OracleNamedPlaylistMutationExecutor.cs`, `MainWindow.NamedPlaylists.cs`, `Web/named-playlist-workflow.js`에 있다. 자동 근거는 named playlist Core/Infrastructure/Web/bridge 테스트이며 실제 기존 데이터의 read-only 대조는 `docs/NAMED_PLAYLIST_READ_AUDIT.md`에 있다. 실제 운영 DB write와 named playlist 전용 scene/asset/PREPARE/PGM 검증은 아직 없다.
### ThemeA — 테마 추가·편집
@@ -281,8 +281,8 @@
| 자동 테스트 | Core 1,259개, Infrastructure 173개, Playout 387개가 Debug/Release x64에서 각각 통과했고 Web workflow·bridge 371/371도 통과했다. 412 action의 폐쇄형 매트릭스, 계약·경계·복원 실패와 native fail-closed gate를 검증한다. |
| 실제 운영 DB write | **미실행.** GraphE, named playlist, ThemeA, EList의 mutation executor는 fake/계약 테스트로 검증했으며 운영 DB에는 쓰지 않았다. FSell/VI는 테스트용 격리 저장소만 사용했다. |
| 실제 Tornado2 PGM | 제한된 승인 회차에서 `5001`, `5074` page 1·2, TAKE OUT을 확인했다. 이는 전체 412 action, GraphE, FSell, VI, ThemeA, EList, 이름 있는 playlist 각각의 UI 동등성 검증이 아니다. |
| PList legacy 복원 | raw restore와 fail-closed materialization 계약은 구현했다. 실제 기존 `DC_TITLE`/`LIST_TEXT`의 한국어 무손상 복원은 적용 중이며 완료 증거가 다. |
| manual named save | GraphE·FSell·VI descriptor 계약과 복원 테스트는 있다. 실제 Oracle save→reload→fresh manual identity/version→page plan→PGM은 미검증이다. |
| PList legacy 복원 | 실제 `DC_TITLE`/`LIST_TEXT` 2,213행의 한국어·7필드 무손상과 typed persistence 일치를 확인했다. 기본 `selectionMapped=1,581`/차단 632와 별도 GraphE 22/14를 합친 결과는 1,603/610이다. selection 매핑은 scene data·asset·PREPARE·PGM 완료 증거가 아니다. |
| manual named save | 현재 legacy PList manual 36행은 모두 GraphE이며 fresh proof로 22행을 복원하고 저장값 무효 9행·identity 중복 5행을 차단했다. FSell/VI PList행은 0건이고 production source read는 FSell 3×5행, VI 9항목·2 page로 전후 불변이다. 실제 Oracle save→reload와 PGM은 미검증이다. |
| ThemeA/EList identity | expected identity·상관 응답 계약은 구현했다. 실제 운영 DB의 KRX/NXT·동명이름·코드 경계와 정확한 선택 행 replace/delete는 미검증이다. |
| legacy file import | `종목비교.dat`·FSell·VI importer는 strict parser, source/hash 재검증, durable intent/marker, rollback·OutcomeUnknown 차단까지 구현했다. 실제 패키지 profile의 명시적 1회 실행은 미검증이며 기존 파일을 덮어쓰지 않는다. |
| 남은 실환경 검증 | 다섯 전용 화면의 Web 연결과 signed `1.0.3.0` Release MSIX package-context 실행은 화면/계약 검증 범위에서 확인됐다. 실제 입력→운영 DB write→재조회와 각 전용 scene PGM 검증은 별도 승인·테스트 데이터·롤백 계획이 있는 회차에서만 수행한다. 운영 DB write 또는 새 Live TAKE IN을 승인 없이 실행하지 않는다. |