feat: migrate legacy playout workflow and scenes

This commit is contained in:
2026-07-10 23:58:45 +09:00
parent 491a740505
commit 8dae7b8e0d
128 changed files with 39177 additions and 205 deletions

View File

@@ -0,0 +1,60 @@
#Requires -Version 5.1
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $CutRoot
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if (-not [IO.Path]::IsPathRooted($CutRoot)) {
throw 'CutRoot must be an absolute path.'
}
$root = [IO.Path]::GetFullPath($CutRoot)
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
throw 'CutRoot does not identify an existing directory.'
}
# Active MainForm aliases for the 34 reachable builders. s8086 intentionally has no alias.
$aliases = @(
'5001', 'N5001', '5006', '5011', '5016', '50160',
'5023', '5024', '5025', '5026', '5029',
'8018', '8032', '5032', '5037',
'5074', '5076', '5077', '5078', '5079',
'5080', '5081', '5082', '5083', '5084', '5085',
'5086', '50860', '5087', '5088',
'6001', '6067', '8001', '8002', '8003',
'8035', '8061', '8040', '8046', '8051', '8056',
'8067', '5068', '5070', '5072'
)
$missing = [Collections.Generic.List[string]]::new()
$unsafe = [Collections.Generic.List[string]]::new()
foreach ($alias in $aliases) {
$path = Join-Path $root ($alias + '.t2s')
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
$missing.Add($alias)
continue
}
$item = Get-Item -LiteralPath $path -Force
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 -or
$item.Length -le 0) {
$unsafe.Add($alias)
}
}
if ($missing.Count -ne 0 -or $unsafe.Count -ne 0) {
throw "Legacy cut coverage failed. Missing=$($missing.Count), Unsafe=$($unsafe.Count)."
}
[pscustomobject][ordered]@{
Status = 'Passed'
ReachableBuilders = 34
ActiveAliases = $aliases.Count
Missing = $missing.Count
Unsafe = $unsafe.Count
}

View File

@@ -0,0 +1,52 @@
[CmdletBinding()]
param(
[string] $LegacyProjectRoot,
[string] $ManifestPath
)
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($LegacyProjectRoot)) {
$LegacyProjectRoot = Join-Path $PSScriptRoot '..\..\MBN_STOCK_N\MBN_STOCK_N'
}
if ([string]::IsNullOrWhiteSpace($ManifestPath)) {
$ManifestPath = Join-Path $PSScriptRoot '..\docs\legacy-scene-source-hashes.json'
}
$legacyRoot = [IO.Path]::GetFullPath($LegacyProjectRoot)
$manifestFile = [IO.Path]::GetFullPath($ManifestPath)
if (-not (Test-Path -LiteralPath $legacyRoot -PathType Container)) {
throw 'The read-only legacy project root was not found.'
}
if (-not (Test-Path -LiteralPath $manifestFile -PathType Leaf)) {
throw 'The legacy scene hash manifest was not found.'
}
$manifest = Get-Content -LiteralPath $manifestFile -Raw -Encoding UTF8 | ConvertFrom-Json
if ($manifest.schemaVersion -ne 1 -or
$manifest.algorithm -ne 'SHA-256' -or
$manifest.builders.Count -ne 35) {
throw 'The legacy scene hash manifest is invalid.'
}
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
foreach ($entry in $manifest.builders) {
if (-not $seen.Add([string] $entry.builder)) {
throw 'The legacy scene hash manifest contains a duplicate builder.'
}
$relativePath = ([string] $entry.sourceFile).Replace('/', [IO.Path]::DirectorySeparatorChar)
$sourceFile = [IO.Path]::GetFullPath((Join-Path $legacyRoot $relativePath))
$rootPrefix = $legacyRoot.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
if (-not $sourceFile.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase) -or
-not (Test-Path -LiteralPath $sourceFile -PathType Leaf)) {
throw "Legacy source is missing for $($entry.builder)."
}
$actual = (Get-FileHash -LiteralPath $sourceFile -Algorithm SHA256).Hash
if (-not $actual.Equals([string] $entry.sha256, [StringComparison]::OrdinalIgnoreCase)) {
throw "Legacy source baseline changed for $($entry.builder)."
}
}
Write-Output "LEGACY_SCENE_BASELINE: PASS ($($seen.Count)/35)"

View File

@@ -56,6 +56,60 @@ foreach ($script in $scripts) {
}
}
$appScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'Web\app.js') -Raw -Encoding UTF8
$mainWindowScript = Get-Content -LiteralPath (Join-Path $repositoryRoot 'MainWindow.xaml.cs') -Raw -Encoding UTF8
$sceneCatalogMatches = [regex]::Matches(
$appScript,
'builderKey:\s*"s[0-9]+"')
if ($sceneCatalogMatches.Count -ne 35) {
throw "Web scene catalog must contain exactly 35 builder entries; found $($sceneCatalogMatches.Count)."
}
$selectionPresetMatches = [regex]::Matches(
$appScript,
'(?m)^\s+s[0-9]+:\s+\[')
if ($selectionPresetMatches.Count -ne 34 -or
$appScript -notmatch 'Every reachable legacy builder must have a Web selection preset') {
throw "Web scene selection presets must cover exactly 34 reachable builders; found $($selectionPresetMatches.Count)."
}
if ($appScript -notmatch 'reachableAliases\.size !== 45' -or
$appScript -notmatch 'sceneCutAlias' -or
$appScript -notmatch 'sceneAliases\(definition\)\.includes\(storedCode\)' -or
$appScript -notmatch 'resolveStoredCatalogIndex') {
throw 'Web scene selection must expose and preserve all 45 active cut aliases.'
}
if ($appScript -match 'payload\.cue' -or
$appScript -notmatch 'payload\.playlist' -or
$appScript -notmatch 'payload\.selectedIndex') {
throw 'Web playout must send a PREPARE playlist snapshot and must not choose the NEXT cue.'
}
if ($appScript -notmatch 'const DEFAULT_FADE_DURATION = 6;' -or
$appScript -notmatch 'fadeDuration: DEFAULT_FADE_DURATION') {
throw 'Web playout default fade must match MainForm ComboDi.SelectedIndex 6.'
}
if ($appScript -notmatch 'normalizeScenePreviewFields' -or
$appScript -notmatch 'currentPageItemCount' -or
$appScript -notmatch 'renderSceneDataPreview') {
throw 'Web playout must render bounded authoritative scene data and complete page state.'
}
if ($appScript -notmatch 'row-enabled' -or
$appScript -notmatch 'item\.enabled = include\.checked' -or
$appScript -notmatch 'isPlaylistSnapshotLocked' -or
$appScript -notmatch 'pending: Boolean\(state\.playout\.pending\)' -or
$appScript -notmatch 'outcomeUnknown: state\.playout\.sessionQuarantined') {
throw 'Web playlist must expose the legacy active flag and freeze its native snapshot.'
}
if ($appScript -notmatch 'postNative\("playout-timeout-quarantine", \{ requestId, command \}\)' -or
$appScript -notmatch 'browserCorrelationQuarantined') {
throw 'Web response timeouts must latch the process-lifetime native quarantine.'
}
if ($mainWindowScript -notmatch 'OnNavigationStarting' -or
$mainWindowScript -notmatch 'OnProcessFailed' -or
$mainWindowScript -notmatch 'ReloadBrowserSafely' -or
$mainWindowScript -notmatch 'QuarantineIfBrowserCorrelationCanBeLost' -or
$mainWindowScript -notmatch 'Volatile\.Read\(ref _playoutCommandInFlight\)') {
throw 'Reload, navigation, and WebView process failure must quarantine in-flight playout correlation.'
}
& $node --test $test
if ($LASTEXITCODE -ne 0) {
throw "Web playout tests failed with exit code $LASTEXITCODE."
@@ -64,5 +118,7 @@ if ($LASTEXITCODE -ne 0) {
[pscustomobject][ordered]@{
Status = 'Passed'
SyntaxFiles = $scripts.Count
SceneCatalogEntries = $sceneCatalogMatches.Count
ReachableSelectionPresets = $selectionPresetMatches.Count
TestFile = [IO.Path]::GetFileName($test)
}