feat: complete legacy parity and modernize operator UI

This commit is contained in:
2026-07-22 12:34:46 +09:00
parent fc4007d676
commit 939c252d23
149 changed files with 26515 additions and 1736 deletions

View File

@@ -47,6 +47,7 @@ try
await VerifyOracleServerVersionAsync(runtime.Executor, cancellation.Token);
await VerifyStockSearchAsync(runtime.Executor, cancellation.Token);
await VerifyDynamicResultCompatibilityAsync(runtime.Executor, cancellation.Token);
await VerifyThemeSelectorAsync(runtime.Executor, cancellation.Token);
await VerifyOverseasSelectorsAsync(runtime.Executor, cancellation.Token);
await VerifyComparisonWorldSelectorAsync(runtime.Executor, cancellation.Token);
@@ -57,7 +58,13 @@ try
runtime.ConnectionFactory,
cancellation.Token);
await VerifyManualFinancialSearchAsync(runtime.Executor, cancellation.Token);
await ManualFinancialListCompatibilityAudit.RunAsync(
runtime.Executor,
cancellation.Token);
await NamedPlaylistReadOnlyDbAudit.RunAsync(runtime.Executor, cancellation.Token);
await NamedPlaylistOperatorCompatibilityAudit.RunAsync(
runtime.Executor,
cancellation.Token);
await NamedManualRestoreReadOnlyAudit.RunAsync(
runtime.Executor,
NamedManualRestoreReadOnlyAudit.DefaultTrustedDirectory(),
@@ -736,6 +743,9 @@ static async Task VerifyOperatorCatalogSchemaAsync(
_ = await themePersistence.SuggestNextCodeAsync(
ThemeMarket.Nxt,
cancellationToken);
await ThemeCatalogStoredEditorCompatibilityAudit.RunAsync(
executor,
cancellationToken);
_ = await new LegacyExpertCatalogPersistenceService(executor)
.SuggestNextCodeAsync(cancellationToken);
var widthSummary = await ReadMariaDbThemeColumnTypesAsync(
@@ -813,6 +823,96 @@ static async Task VerifyStockSearchAsync(
$"({string.Join(", ", counts)})");
}
static async Task VerifyDynamicResultCompatibilityAsync(
IDataQueryExecutor executor,
CancellationToken cancellationToken)
{
if (LegacyOperatorCatalogWorkflowController.MaximumCatalogResults !=
LegacyThemeSelectionService.MaximumResults ||
LegacyOperatorCatalogWorkflowController.MaximumStockResults !=
LegacyStockSearchService.MaximumResults ||
LegacyManualFinancialWorkflow.MaximumStockResults !=
LegacyStockSearchService.MaximumResults ||
LegacyManualListsWorkflow.MaximumSearchResults <
LegacyStockSearchService.MaximumResults)
{
throw new DatabaseConfigurationException(
"A dynamic operator list is still bounded below its Core compatibility ceiling.");
}
var worldService = new LegacyWorldStockSearchService(executor);
var comparison = new LegacyComparisonDataService(
new LegacyParityStockSearchService(executor),
worldService);
Console.WriteLine("DynamicResultCompatibility domestic: BEGIN");
var domestic = await comparison.SearchDomesticWithDiagnosticsAsync(
string.Empty,
LegacyStockSearchService.MaximumResults,
cancellationToken);
if (domestic.Items.Count != 4_599 ||
domestic.IsolatedInvalidRowCount != 0 ||
domestic.IsTruncated)
{
throw new DatabaseConfigurationException(
"The UC3 domestic compatibility cardinality changed: expected 4,599 safe rows.");
}
Console.WriteLine("DynamicResultCompatibility world: BEGIN");
var coreWorld = await worldService.SearchAsync(
string.Empty,
LegacyWorldStockSearchService.MaximumResults,
cancellationToken);
var commaNameCount = coreWorld.Items.Count(item => item.InputName.Contains(','));
if (coreWorld.Items.Count != 1_474 ||
coreWorld.IsolatedInvalidRowCount != 16 ||
commaNameCount != 1)
{
throw new DatabaseConfigurationException(
"The raw UC3 world compatibility split changed before application mapping.");
}
var world = await comparison.SearchWorldWithDiagnosticsAsync(
string.Empty,
LegacyWorldStockSearchService.MaximumResults,
cancellationToken);
var worldSourceRows = checked(world.Items.Count + world.IsolatedInvalidRowCount);
var worldActionableRows = world.Items.Count(item => item.CanSelect);
Console.WriteLine(
"DynamicResultCompatibility world diagnostics: " +
$"source={worldSourceRows.ToString(CultureInfo.InvariantCulture)} " +
$"visible={world.Items.Count.ToString(CultureInfo.InvariantCulture)} " +
$"actionable={worldActionableRows.ToString(CultureInfo.InvariantCulture)} " +
$"isolated={world.IsolatedInvalidRowCount.ToString(CultureInfo.InvariantCulture)} " +
$"deferred={world.DeferredUnsafeRowCount.ToString(CultureInfo.InvariantCulture)} " +
$"truncated={world.IsTruncated}");
if (worldSourceRows != 1_490 ||
world.Items.Count != 1_474 ||
worldActionableRows != 1_473 ||
world.IsolatedInvalidRowCount != 16 ||
world.DeferredUnsafeRowCount != 1 ||
!world.IsTruncated)
{
throw new DatabaseConfigurationException(
"The UC3 world compatibility cardinality changed: expected 1,490 source rows, " +
"1,474 visible rows, 1,473 selectable rows, 16 isolated blank-display rows, " +
"and one deferred comma-subject row.");
}
Console.WriteLine(
"DynamicResultCompatibility: " +
$"domesticSource={domestic.Items.Count.ToString(CultureInfo.InvariantCulture)} " +
$"domesticVisible={domestic.Items.Count.ToString(CultureInfo.InvariantCulture)} " +
$"domesticIsolated={domestic.IsolatedInvalidRowCount.ToString(CultureInfo.InvariantCulture)}; " +
$"worldSource={worldSourceRows.ToString(CultureInfo.InvariantCulture)} " +
$"worldVisible={world.Items.Count.ToString(CultureInfo.InvariantCulture)} " +
$"worldActionable={worldActionableRows.ToString(CultureInfo.InvariantCulture)} " +
$"worldIsolated={world.IsolatedInvalidRowCount.ToString(CultureInfo.InvariantCulture)} " +
$"worldDeferred={world.DeferredUnsafeRowCount.ToString(CultureInfo.InvariantCulture)}; " +
$"stockCeiling={LegacyStockSearchService.MaximumResults.ToString(CultureInfo.InvariantCulture)} " +
$"worldCeiling={LegacyWorldStockSearchService.MaximumResults.ToString(CultureInfo.InvariantCulture)} " +
$"themeCeiling={LegacyThemeSelectionService.MaximumResults.ToString(CultureInfo.InvariantCulture)} " +
$"viCeiling={LegacyManualListsWorkflow.MaximumSearchResults.ToString(CultureInfo.InvariantCulture)}");
}
static async Task VerifyThemeSelectorAsync(
IDataQueryExecutor executor,
CancellationToken cancellationToken)
@@ -822,22 +922,81 @@ static async Task VerifyThemeSelectorAsync(
var themes = await service.SearchAsync(
string.Empty,
ThemeSession.PreMarket,
maximumResults: 100,
maximumResults: LegacyThemeSelectionService.MaximumResults,
cancellationToken);
if (themes.Items.Count == 0)
{
throw new DatabaseConfigurationException(
"The production theme selector returned no unambiguous canonical rows.");
}
if (themes.IsTruncated)
{
throw new DatabaseConfigurationException(
"The production theme selector exceeded its finite " +
$"{LegacyThemeSelectionService.MaximumResults.ToString(CultureInfo.InvariantCulture)}-row safety ceiling.");
}
var storedNxtSuffixCount = themes.Items.Count(item =>
item.Identity.ThemeTitle.Contains("(NXT)", StringComparison.Ordinal));
var storedNxtSuffixKrxCount = themes.Items.Count(item =>
item.Identity.Market == ThemeMarket.Krx &&
item.Identity.ThemeTitle.Contains("(NXT)", StringComparison.Ordinal));
var storedNxtSuffixNxtCount = themes.Items.Count(item =>
item.Identity.Market == ThemeMarket.Nxt &&
item.Identity.ThemeTitle.Contains("(NXT)", StringComparison.Ordinal));
Console.WriteLine(
"ThemeSelector identity diagnostics: " +
$"storedNxtSuffix={storedNxtSuffixCount.ToString(CultureInfo.InvariantCulture)} " +
$"krx={storedNxtSuffixKrxCount.ToString(CultureInfo.InvariantCulture)} " +
$"nxt={storedNxtSuffixNxtCount.ToString(CultureInfo.InvariantCulture)}");
var workflow = new LegacyThemeWorkflow(service);
LegacyThemeWorkflowSnapshot workflowSnapshot;
try
{
workflowSnapshot = await workflow.SearchAsync(
workflow.CreateInitial(ThemeSession.PreMarket),
string.Empty,
ThemeSession.PreMarket,
cancellationToken);
}
catch (LegacyThemeWorkflowDataException exception)
{
Console.WriteLine(
"THEME_WORKFLOW_BOUNDARY: " + exception.Message);
throw new DatabaseConfigurationException(
"The production theme rows passed the data service but failed the operator workflow: " +
exception.Message);
}
if (workflowSnapshot.SearchIsTruncated ||
workflowSnapshot.SearchResults.Count != themes.Items.Count)
{
throw new DatabaseConfigurationException(
"The production theme operator workflow did not preserve the complete selector result.");
}
await ThemeSelectorCompatibilityAudit.RunAsync(
executor,
service,
themes,
workflow,
workflowSnapshot,
cancellationToken);
var selected = themes.Items[0];
var preview = await service.PreviewAsync(
selected.Identity,
maximumItems: 100,
cancellationToken);
var marketCounts = themes.Items
.GroupBy(item => item.Identity.Market)
.OrderBy(group => group.Key)
.Select(group =>
$"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}");
Console.WriteLine(
$"ThemeSelector: themes={themes.Items.Count.ToString(CultureInfo.InvariantCulture)}, " +
$"preview={preview.Items.Count.ToString(CultureInfo.InvariantCulture)}");
$"ThemeSelector: themes={themes.Items.Count.ToString(CultureInfo.InvariantCulture)} " +
$"({string.Join(", ", marketCounts)}), " +
$"preview={preview.Items.Count.ToString(CultureInfo.InvariantCulture)}, " +
$"truncated={themes.IsTruncated}");
}
static async Task VerifyOverseasSelectorsAsync(
@@ -873,7 +1032,9 @@ static async Task VerifyOverseasSelectorsAsync(
LegacyOverseasStockSearchService.DefaultMaximumResults,
cancellationToken);
if (uc5Snapshot.Stock.Status != LegacyOverseasSearchStatus.Ready ||
uc5Snapshot.Stock.ResultCount != overseasStocks.Items.Count)
uc5Snapshot.Stock.ResultCount != overseasStocks.Items.Count ||
uc5Snapshot.Stock.IsolatedInvalidRowCount !=
overseasStocks.IsolatedInvalidRowCount)
{
throw new DatabaseConfigurationException(
"The full UC5 overseas-stock workflow rejected current master rows.");
@@ -884,11 +1045,21 @@ static async Task VerifyOverseasSelectorsAsync(
throw new DatabaseConfigurationException(
"The production overseas selectors returned no rows.");
}
if (industries.Items.Count != 53 ||
overseasStocks.Items.Count != 1_474 ||
overseasStocks.IsolatedInvalidRowCount != 16 ||
!overseasStocks.IsTruncated)
{
throw new DatabaseConfigurationException(
"The UC5 compatibility cardinality changed: expected 53 industries, " +
"1,474 selectable US/TW stocks, and 16 isolated blank-name rows.");
}
Console.WriteLine(
$"OverseasSelectors: industries={industries.Items.Count.ToString(CultureInfo.InvariantCulture)}, " +
$"comparisonStocks={stocks.Items.Count.ToString(CultureInfo.InvariantCulture)}, " +
$"uc5Stocks={overseasStocks.Items.Count.ToString(CultureInfo.InvariantCulture)}");
$"uc5Stocks={overseasStocks.Items.Count.ToString(CultureInfo.InvariantCulture)}, " +
$"isolatedBlankNames={overseasStocks.IsolatedInvalidRowCount.ToString(CultureInfo.InvariantCulture)}");
}
static async Task PrintUc5OverseasStockDiagnosticsAsync(
@@ -908,7 +1079,11 @@ static async Task PrintUc5OverseasStockDiagnosticsAsync(
""";
var spec = new DataQuerySpec(
sql,
[new DataQueryParameter("row_limit", 501, DbType.Int32)]);
[new DataQueryParameter(
"row_limit",
LegacyOverseasStockSearchService.MaximumResults +
LegacyOverseasStockSearchService.MaximumIsolatedRows + 1,
DbType.Int32)]);
spec.ValidateFor(DataSourceKind.Oracle);
var table = await executor.ExecuteAsync(
DataSourceKind.Oracle,
@@ -1009,13 +1184,40 @@ static async Task VerifyExpertSelectorAsync(
"The production expert selector returned no rows.");
}
var preview = await service.PreviewAsync(
experts.Items[0].Identity,
maximumItems: 100,
cancellationToken);
if (experts.Items.Count != 6 || experts.IsTruncated)
{
throw new DatabaseConfigurationException(
"The expert compatibility cardinality changed: expected all 6 experts.");
}
var recommendationCount = 0;
var missingBuyAmountCount = 0;
foreach (var expert in experts.Items)
{
var preview = await service.PreviewAsync(
expert.Identity,
maximumItems: 100,
cancellationToken);
if (preview.IsTruncated)
{
throw new DatabaseConfigurationException(
"An expert preview exceeded the complete compatibility boundary.");
}
recommendationCount += preview.Items.Count;
missingBuyAmountCount += preview.Items.Count(item => item.BuyAmount is null);
}
if (recommendationCount != 32 || missingBuyAmountCount != 5)
{
throw new DatabaseConfigurationException(
"The expert recommendation compatibility cardinality changed: " +
"expected 32 rows including 5 blank buy amounts.");
}
Console.WriteLine(
$"ExpertSelector: experts={experts.Items.Count.ToString(CultureInfo.InvariantCulture)}, " +
$"preview={preview.Items.Count.ToString(CultureInfo.InvariantCulture)}");
$"recommendations={recommendationCount.ToString(CultureInfo.InvariantCulture)}, " +
$"blankBuyAmounts={missingBuyAmountCount.ToString(CultureInfo.InvariantCulture)}");
}
static async Task VerifyTradingHaltSelectorAsync(