V13-FE-011: finalize search list layout slice

This commit is contained in:
2026-08-09 02:57:26 +09:00
parent 9efd202e76
commit 6422cb2b13
984 changed files with 120811 additions and 1498 deletions
@@ -0,0 +1,48 @@
// generated from contracts/configuration/kbx.configuration.json; do not edit.
namespace Kbx.Shared.Configuration.Generated;
public sealed record KbxConfigurationSettingDefinition(string Key,string Env,string Category,string Type,bool Secret,bool RestartRequired,string? DefaultValue,string[] AllowedValues,string[] RequiredIn,string? RequiredWhenKey,string? RequiredWhenEquals,int? Minimum,int? Maximum,string[] AllowedSources);
public sealed record KbxEnvironmentProfileDefinition(string Id,bool ArtifactPromotion,string MigrationStrategy,string ProviderNetwork,bool RequireHttps,bool ProviderEndpointOverrideAllowed,string[] RequiredGates);
public static class KbxConfigurationCatalog {
public const string SourceSha256="e09186d626b034a524b7cdf78f1e4bb6adbc112975b3baf4fd6194b1c1abd771";
public static readonly IReadOnlyDictionary<string,KbxConfigurationSettingDefinition> Settings=new Dictionary<string,KbxConfigurationSettingDefinition>(StringComparer.Ordinal) {
["Kbx:Runtime:Environment"] = new("Kbx:Runtime:Environment","KBX__Runtime__Environment","runtime","enum",false,true,"Development",new string[]{"Development","Test","Staging","Production"},new string[]{"Development","Test","Staging","Production"},null,null,null,null,new string[]{}),
["Kbx:Runtime:ReadOnly"] = new("Kbx:Runtime:ReadOnly","KBX__Runtime__ReadOnly","runtime","boolean",false,false,"false",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["ConnectionStrings:Main"] = new("ConnectionStrings:Main","ConnectionStrings__Main","database","connection-string",true,true,null,new string[]{},new string[]{"Development","Test","Staging","Production"},null,null,null,null,new string[]{"environment","secret-store","user-secrets"}),
["Kbx:Database:MigrationsMode"] = new("Kbx:Database:MigrationsMode","KBX__Database__MigrationsMode","database","enum",false,true,"validate",new string[]{"validate","startup-apply","predeploy"},new string[]{"Development","Test","Staging","Production"},null,null,null,null,new string[]{}),
["Kbx:Database:CommandTimeoutSeconds"] = new("Kbx:Database:CommandTimeoutSeconds","KBX__Database__CommandTimeoutSeconds","database","integer",false,true,"30",new string[]{},new string[]{},null,null,1,300,new string[]{}),
["Kbx:Hangfire:Enabled"] = new("Kbx:Hangfire:Enabled","KBX__Hangfire__Enabled","background-jobs","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Hangfire:WorkerCount"] = new("Kbx:Hangfire:WorkerCount","KBX__Hangfire__WorkerCount","background-jobs","integer",false,true,"4",new string[]{},new string[]{},null,null,1,64,new string[]{}),
["Kbx:SignalR:Enabled"] = new("Kbx:SignalR:Enabled","KBX__SignalR__Enabled","realtime","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Outbox:DispatcherEnabled"] = new("Kbx:Outbox:DispatcherEnabled","KBX__Outbox__DispatcherEnabled","messaging","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Inbox:CleanupEnabled"] = new("Kbx:Inbox:CleanupEnabled","KBX__Inbox__CleanupEnabled","messaging","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Integration:DispatcherEnabled"] = new("Kbx:Integration:DispatcherEnabled","KBX__Integration__DispatcherEnabled","integration","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:ExternalData:RefreshEnabled"] = new("Kbx:ExternalData:RefreshEnabled","KBX__ExternalData__RefreshEnabled","external-data","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:ExternalData:ObservationRetentionEnabled"] = new("Kbx:ExternalData:ObservationRetentionEnabled","KBX__ExternalData__ObservationRetentionEnabled","external-data","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Experiments:Enabled"] = new("Kbx:Experiments:Enabled","KBX__Experiments__Enabled","experiments","boolean",false,false,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Logging:MinimumLevel"] = new("Kbx:Logging:MinimumLevel","KBX__Logging__MinimumLevel","logging","enum",false,true,"Information",new string[]{"Debug","Information","Warning","Error"},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Logging:JsonConsoleEnabled"] = new("Kbx:Logging:JsonConsoleEnabled","KBX__Logging__JsonConsoleEnabled","logging","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Telemetry:Enabled"] = new("Kbx:Telemetry:Enabled","KBX__Telemetry__Enabled","observability","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Telemetry:OtlpEndpoint"] = new("Kbx:Telemetry:OtlpEndpoint","KBX__Telemetry__OtlpEndpoint","observability","uri",false,true,null,new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Telegram:Enabled"] = new("Kbx:Telegram:Enabled","KBX__Telegram__Enabled","alerting","boolean",false,true,"false",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Telegram:BotToken"] = new("Kbx:Telegram:BotToken","KBX__Telegram__BotToken","alerting","string",true,true,null,new string[]{},new string[]{},"Kbx:Telegram:Enabled","true",null,null,new string[]{"environment","secret-store","user-secrets"}),
["Kbx:Telegram:ChatId"] = new("Kbx:Telegram:ChatId","KBX__Telegram__ChatId","alerting","string",true,true,null,new string[]{},new string[]{},"Kbx:Telegram:Enabled","true",null,null,new string[]{"environment","secret-store","user-secrets"}),
["ExternalProviders:Krx:Enabled"] = new("ExternalProviders:Krx:Enabled","ExternalProviders__Krx__Enabled","provider","boolean",false,true,"false",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["ExternalProviders:Krx:AuthKey"] = new("ExternalProviders:Krx:AuthKey","ExternalProviders__Krx__AuthKey","provider","string",true,true,null,new string[]{},new string[]{},"ExternalProviders:Krx:Enabled","true",null,null,new string[]{"environment","secret-store","user-secrets"}),
["ExternalProviders:OpenDart:Enabled"] = new("ExternalProviders:OpenDart:Enabled","ExternalProviders__OpenDart__Enabled","provider","boolean",false,true,"false",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["ExternalProviders:OpenDart:ApiKey"] = new("ExternalProviders:OpenDart:ApiKey","ExternalProviders__OpenDart__ApiKey","provider","string",true,true,null,new string[]{},new string[]{},"ExternalProviders:OpenDart:Enabled","true",null,null,new string[]{"environment","secret-store","user-secrets"}),
["ExternalProviders:Kis:Enabled"] = new("ExternalProviders:Kis:Enabled","ExternalProviders__Kis__Enabled","provider","boolean",false,true,"false",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["ExternalProviders:Kis:Environment"] = new("ExternalProviders:Kis:Environment","ExternalProviders__Kis__Environment","provider","enum",false,true,"sandbox",new string[]{"sandbox","production"},new string[]{},"ExternalProviders:Kis:Enabled","true",null,null,new string[]{}),
["ExternalProviders:Kis:AppKey"] = new("ExternalProviders:Kis:AppKey","ExternalProviders__Kis__AppKey","provider","string",true,true,null,new string[]{},new string[]{},"ExternalProviders:Kis:Enabled","true",null,null,new string[]{"environment","secret-store","user-secrets"}),
["ExternalProviders:Kis:AppSecret"] = new("ExternalProviders:Kis:AppSecret","ExternalProviders__Kis__AppSecret","provider","string",true,true,null,new string[]{},new string[]{},"ExternalProviders:Kis:Enabled","true",null,null,new string[]{"environment","secret-store","user-secrets"}),
["Kbx:Security:RequireHttps"] = new("Kbx:Security:RequireHttps","KBX__Security__RequireHttps","security","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Health:ReadinessEnabled"] = new("Kbx:Health:ReadinessEnabled","KBX__Health__ReadinessEnabled","runtime","boolean",false,true,"true",new string[]{},new string[]{},null,null,null,null,new string[]{}),
["Kbx:Import:MaxUploadBytes"] = new("Kbx:Import:MaxUploadBytes","KBX__Import__MaxUploadBytes","import","integer",false,true,"52428800",new string[]{},new string[]{},null,null,1048576,209715200,new string[]{}),
["Kbx:Import:MaxRows"] = new("Kbx:Import:MaxRows","KBX__Import__MaxRows","import","integer",false,true,"100000",new string[]{},new string[]{},null,null,1,1000000,new string[]{})
};
public static readonly IReadOnlyDictionary<string,KbxEnvironmentProfileDefinition> Environments=new Dictionary<string,KbxEnvironmentProfileDefinition>(StringComparer.Ordinal) {
["Development"] = new("Development",false,"startup-apply-allowed","opt-in",false,true,new string[]{"static-governance"}),
["Test"] = new("Test",false,"ephemeral-apply","forbidden",false,true,new string[]{"static-governance","build","unit","integration","scenario"}),
["Staging"] = new("Staging",true,"predeploy","opt-in",true,false,new string[]{"static-governance","build","unit","integration","scenario","migration-dry-run"}),
["Production"] = new("Production",true,"predeploy","opt-in",true,false,new string[]{"static-governance","build","unit","integration","scenario","migration-dry-run","configuration-validation","release-governance"})
};
}
@@ -0,0 +1,22 @@
namespace Kbx.Shared.Configuration;
public sealed record KbxConfigurationIssue(
string Code,
string Key,
string Message,
bool Fatal);
public sealed record KbxConfigurationValidationResult(
string Environment,
string NonSecretFingerprint,
IReadOnlyList<KbxConfigurationIssue> Issues)
{
public bool IsValid => Issues.All(x => !x.Fatal);
}
public sealed record KbxDeploymentDescriptor(
string Environment,
string ApplicationVersion,
string KbxContractVersion,
string NonSecretConfigurationFingerprint,
DateTimeOffset StartedAt);
@@ -0,0 +1,20 @@
using System.Security.Cryptography;
using System.Text;
using Kbx.Shared.Configuration.Generated;
using Microsoft.Extensions.Configuration;
namespace Kbx.Shared.Configuration;
public static class KbxConfigurationFingerprint
{
public static string Compute(IConfiguration configuration)
{
var lines = KbxConfigurationCatalog.Settings.Values
.Where(x => !x.Secret)
.OrderBy(x => x.Key, StringComparer.Ordinal)
.Select(x => $"{x.Key}={configuration[x.Key] ?? x.DefaultValue ?? string.Empty}");
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("\n", lines)));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
@@ -0,0 +1,13 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Kbx.Shared.Configuration;
public static class KbxConfigurationRegistration
{
public static IServiceCollection AddKbxConfigurationGovernance(this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton(new KbxConfigurationStartupValidator(configuration));
return services;
}
}
@@ -0,0 +1,70 @@
using Kbx.Shared.Configuration.Generated;
using Microsoft.Extensions.Configuration;
namespace Kbx.Shared.Configuration;
public sealed class KbxConfigurationStartupValidator(IConfiguration configuration)
{
private static readonly string[] PlaceholderSecrets = ["changeme", "change-me", "example", "secret", "password", "todo"];
public KbxConfigurationValidationResult Validate()
{
var environment = configuration["Kbx:Runtime:Environment"] ?? "Development";
var issues = new List<KbxConfigurationIssue>();
if (!KbxConfigurationCatalog.Environments.TryGetValue(environment, out _))
issues.Add(new("CONFIG_ENVIRONMENT_UNKNOWN", "Kbx:Runtime:Environment", $"Unknown KBX environment '{environment}'.", true));
foreach (var setting in KbxConfigurationCatalog.Settings.Values)
{
var value = configuration[setting.Key] ?? setting.DefaultValue;
if (setting.RequiredIn.Contains(environment, StringComparer.Ordinal) && string.IsNullOrWhiteSpace(value))
issues.Add(new("CONFIG_REQUIRED", setting.Key, "Required configuration is missing.", true));
if (setting.RequiredWhenKey is not null &&
string.Equals(configuration[setting.RequiredWhenKey], setting.RequiredWhenEquals, StringComparison.OrdinalIgnoreCase) &&
string.IsNullOrWhiteSpace(value))
issues.Add(new("CONFIG_DEPENDENCY_REQUIRED", setting.Key, $"{setting.Key} is required when {setting.RequiredWhenKey}={setting.RequiredWhenEquals}.", true));
if (setting.Secret && !string.IsNullOrWhiteSpace(value) && PlaceholderSecrets.Any(x => string.Equals(x, value, StringComparison.OrdinalIgnoreCase)))
issues.Add(new("CONFIG_SECRET_PLACEHOLDER", setting.Key, "Secret configuration still contains a placeholder value.", true));
if (setting.AllowedValues.Length > 0 && !string.IsNullOrWhiteSpace(value) && !setting.AllowedValues.Contains(value, StringComparer.OrdinalIgnoreCase))
issues.Add(new("CONFIG_ENUM", setting.Key, $"Unsupported configured value for {setting.Key}.", true));
if (setting.Type == "integer" && !string.IsNullOrWhiteSpace(value))
{
if (!int.TryParse(value, out var number)) issues.Add(new("CONFIG_INTEGER", setting.Key, "Configuration must be an integer.", true));
else
{
if (setting.Minimum is not null && number < setting.Minimum) issues.Add(new("CONFIG_MINIMUM", setting.Key, "Configuration is below the allowed minimum.", true));
if (setting.Maximum is not null && number > setting.Maximum) issues.Add(new("CONFIG_MAXIMUM", setting.Key, "Configuration exceeds the allowed maximum.", true));
}
}
if (setting.Type == "uri" && !string.IsNullOrWhiteSpace(value) && !Uri.TryCreate(value, UriKind.Absolute, out _))
issues.Add(new("CONFIG_URI", setting.Key, "Configuration must be an absolute URI.", true));
}
if (string.Equals(environment, "Production", StringComparison.Ordinal))
{
if (!string.Equals(configuration["Kbx:Database:MigrationsMode"] ?? "validate", "predeploy", StringComparison.OrdinalIgnoreCase))
issues.Add(new("CONFIG_PRODUCTION_MIGRATION_MODE", "Kbx:Database:MigrationsMode", "Production requires predeploy migrations; startup schema mutation is forbidden.", true));
if (!bool.TryParse(configuration["Kbx:Security:RequireHttps"], out var https) || !https)
issues.Add(new("CONFIG_PRODUCTION_HTTPS", "Kbx:Security:RequireHttps", "Production requires HTTPS.", true));
if (string.Equals(configuration["Kbx:Logging:MinimumLevel"], "Debug", StringComparison.OrdinalIgnoreCase))
issues.Add(new("CONFIG_PRODUCTION_DEBUG_LOGGING", "Kbx:Logging:MinimumLevel", "Production Debug logging is forbidden by the KBX reference policy.", true));
}
return new(environment, KbxConfigurationFingerprint.Compute(configuration), issues);
}
public KbxConfigurationValidationResult ValidateOrThrow()
{
var result = Validate();
if (!result.IsValid)
throw new InvalidOperationException("KBX configuration validation failed: " + string.Join(" | ", result.Issues.Where(x => x.Fatal).Select(x => $"{x.Code}:{x.Key}")));
return result;
}
}
@@ -0,0 +1,35 @@
using Microsoft.Extensions.Configuration;
using Xunit;
namespace Kbx.Shared.Configuration.Tests;
public sealed class KbxConfigurationContractTests
{
[Fact]
public void Production_rejects_startup_apply()
{
var values = new Dictionary<string,string?> {
["Kbx:Runtime:Environment"]="Production",
["Kbx:Database:MigrationsMode"]="startup-apply",
["Kbx:Security:RequireHttps"]="true",
["ConnectionStrings:Main"]="Host=test;Database=test"
};
var config = new ConfigurationBuilder().AddInMemoryCollection(values).Build();
var result = new KbxConfigurationStartupValidator(config).Validate();
Assert.Contains(result.Issues, x => x.Code == "CONFIG_PRODUCTION_MIGRATION_MODE");
}
[Fact]
public void Provider_secret_is_required_only_when_enabled()
{
var values = new Dictionary<string,string?> {
["Kbx:Runtime:Environment"]="Development",
["Kbx:Database:MigrationsMode"]="validate",
["ConnectionStrings:Main"]="Host=test;Database=test",
["ExternalProviders:Krx:Enabled"]="true"
};
var config = new ConfigurationBuilder().AddInMemoryCollection(values).Build();
var result = new KbxConfigurationStartupValidator(config).Validate();
Assert.Contains(result.Issues, x => x.Key == "ExternalProviders:Krx:AuthKey");
}
}