Initial commit: Add project files
ci / backend (push) Failing after 12s
ci / frontend (push) Failing after 19s
ci / static (push) Failing after 45s

This commit is contained in:
2026-08-02 05:15:36 +09:00
commit dcd1322d41
636 changed files with 122352 additions and 0 deletions
@@ -0,0 +1,16 @@
namespace KArtSell.BuildingBlocks.Auditing;
/// <summary>
/// Append-only audit envelope. Corrections are new events referencing the original event.
/// </summary>
public sealed record AuditEvent(
Guid AuditEventId,
string EntityType,
string EntityId,
string EventType,
string ActorId,
string CorrelationId,
string PayloadJson,
string PayloadHash,
Guid? CorrectsAuditEventId,
DateTimeOffset OccurredAt);
@@ -0,0 +1,11 @@
namespace KArtSell.BuildingBlocks.Capabilities;
public sealed class CapabilityOptions
{
public const string SectionName = "Capabilities";
public bool AutomaticOrder { get; init; }
public bool KisOrderAdapter { get; init; }
public bool ClientPublication { get; init; }
public bool ShadowEvaluation { get; init; }
}
@@ -0,0 +1,8 @@
using System.Data.Common;
namespace KArtSell.BuildingBlocks.Data;
public interface IDbConnectionFactory
{
ValueTask<DbConnection> OpenAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,10 @@
using System.Data.Common;
using Npgsql;
namespace KArtSell.BuildingBlocks.Data;
public sealed class NpgsqlConnectionFactory(NpgsqlDataSource dataSource) : IDbConnectionFactory
{
public async ValueTask<DbConnection> OpenAsync(CancellationToken cancellationToken = default)
=> await dataSource.OpenConnectionAsync(cancellationToken);
}
@@ -0,0 +1,39 @@
namespace KArtSell.BuildingBlocks.DataIntegrity;
public enum DataQualityStatus
{
Pass,
Warn,
Quarantined
}
/// <summary>
/// Point-in-time metadata shared by canonical data records.
/// PublishedAt controls the earliest market session in which the record may be used.
/// </summary>
public sealed record PitRecordMetadata(
string SourceId,
DateTimeOffset SourceEventTime,
DateTimeOffset PublishedAt,
DateTimeOffset IngestedAt,
DateTimeOffset EffectiveFrom,
DateTimeOffset? EffectiveTo,
int RevisionNo,
string ContentHash,
string DatasetId,
DataQualityStatus QualityStatus)
{
public void EnsureValid()
{
if (string.IsNullOrWhiteSpace(SourceId)) throw new ArgumentException("SourceId is required.");
if (string.IsNullOrWhiteSpace(ContentHash)) throw new ArgumentException("ContentHash is required.");
if (string.IsNullOrWhiteSpace(DatasetId)) throw new ArgumentException("DatasetId is required.");
if (RevisionNo < 0) throw new ArgumentOutOfRangeException(nameof(RevisionNo));
if (IngestedAt < PublishedAt) throw new ArgumentException("IngestedAt cannot precede PublishedAt.");
if (EffectiveTo is not null && EffectiveTo < EffectiveFrom)
throw new ArgumentException("EffectiveTo cannot precede EffectiveFrom.");
}
public bool IsUsableAt(DateTimeOffset decisionCutoff)
=> QualityStatus is DataQualityStatus.Pass && PublishedAt <= decisionCutoff;
}
@@ -0,0 +1,19 @@
using KArtSell.BuildingBlocks.Versioning;
namespace KArtSell.BuildingBlocks.Execution;
public sealed record ExecutionEnvelope(
DateTimeOffset AsOf,
DateTimeOffset PublishedAtCutoff,
string ScopeKey,
string IdempotencyKey,
string CorrelationId,
VersionSet VersionSet,
string ContentHash)
{
public void EnsureValid()
{
if (PublishedAtCutoff > AsOf) throw new InvalidOperationException("PublishedAtCutoff cannot be after AsOf.");
if (string.IsNullOrWhiteSpace(ScopeKey) || string.IsNullOrWhiteSpace(IdempotencyKey) || string.IsNullOrWhiteSpace(CorrelationId) || string.IsNullOrWhiteSpace(ContentHash))
throw new InvalidOperationException("Execution envelope identifiers are required.");
VersionSet.EnsureValid();
}
}
@@ -0,0 +1,10 @@
using System.Security.Cryptography;
using System.Text;
namespace KArtSell.BuildingBlocks.Hashing;
public static class ContentHasher
{
public static string Sha256(string value)
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value))).ToLowerInvariant();
}
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="Dapper" />
<PackageReference Include="Npgsql" />
</ItemGroup>
</Project>
@@ -0,0 +1,21 @@
namespace KArtSell.BuildingBlocks.ReadModels;
public sealed record ProjectionCheckpoint(
string ProjectionName,
int ProjectionVersion,
string SourceWatermark,
Guid? LastEventId,
string ContentHash,
DateTimeOffset BuiltAt)
{
public bool IsStale(DateTimeOffset now, TimeSpan maximumAge) => now - BuiltAt > maximumAge;
}
public sealed record ProjectionRebuildRequest(
Guid RebuildId,
string ProjectionName,
int TargetVersion,
string ThroughWatermark,
bool DryRun,
string RequestedBy,
DateTimeOffset RequestedAt);
@@ -0,0 +1,12 @@
namespace KArtSell.BuildingBlocks.ReadModels;
public enum ProjectionHealth { Current, Stale, Rebuilding, Failed, Quarantined }
public sealed record ProjectionContract(string Name, int Version, string SourceWatermark, DateTimeOffset BuiltAt, TimeSpan StaleAfter, string Owner)
{
public ProjectionHealth Evaluate(DateTimeOffset now, bool rebuilding = false, bool failed = false, bool quarantined = false)
{
if (quarantined) return ProjectionHealth.Quarantined;
if (failed) return ProjectionHealth.Failed;
if (rebuilding) return ProjectionHealth.Rebuilding;
return now - BuiltAt > StaleAfter ? ProjectionHealth.Stale : ProjectionHealth.Current;
}
}
@@ -0,0 +1,31 @@
using System.Data.Common;
using Dapper;
namespace KArtSell.BuildingBlocks.Reliability;
public sealed class DapperInboxStore : IInboxStore
{
private const string Sql = """
insert into building_blocks.inbox_message
(consumer, message_id, received_at, payload_hash)
values (@Consumer, @MessageId, @ReceivedAt, @PayloadHash)
on conflict (consumer, message_id) do nothing;
""";
public async Task<bool> TryBeginAsync(
DbConnection connection,
DbTransaction transaction,
string consumer,
Guid messageId,
string payloadHash,
DateTimeOffset receivedAt,
CancellationToken cancellationToken)
{
var affected = await connection.ExecuteAsync(new CommandDefinition(
Sql,
new { Consumer = consumer, MessageId = messageId, ReceivedAt = receivedAt, PayloadHash = payloadHash },
transaction,
cancellationToken: cancellationToken));
return affected == 1;
}
}
@@ -0,0 +1,93 @@
using Dapper;
using KArtSell.BuildingBlocks.Data;
namespace KArtSell.BuildingBlocks.Reliability;
public sealed class DapperJobRunRepository(IDbConnectionFactory connectionFactory) : IJobRunRepository
{
private const string InsertSql = """
insert into building_blocks.job_run
(job_run_id, job_type, job_version, scope_key, idempotency_key, watermark,
app_version, model_version, config_version, data_version, contract_version,
status, input_hash, started_at, heartbeat_at, trace_id)
values
(@JobRunId, @JobType, @JobVersion, @ScopeKey, @IdempotencyKey, @Watermark,
@CodeSha, @ModelVersion, @ConfigVersion, @DatasetId, @ContractVersion,
'Running', @InputHash, @RequestedAt, @RequestedAt, @TraceId)
on conflict (idempotency_key) do nothing;
""";
private const string HeartbeatSql = """
update building_blocks.job_run
set heartbeat_at = @At
where job_run_id = @JobRunId and status = 'Running';
""";
private const string CompleteSql = """
update building_blocks.job_run
set status = @Status,
output_hash = @OutputHash,
watermark = coalesce(@Watermark, watermark),
error_code = @ErrorCode,
finished_at = @FinishedAt,
heartbeat_at = @FinishedAt
where job_run_id = @JobRunId and status = 'Running';
""";
public async Task<bool> TryStartAsync(JobRunStart start, CancellationToken cancellationToken)
{
start.VersionSet.EnsureValid();
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
var affected = await connection.ExecuteAsync(new CommandDefinition(
InsertSql,
new
{
start.JobRunId,
start.JobType,
start.JobVersion,
start.ScopeKey,
start.IdempotencyKey,
start.Watermark,
start.VersionSet.CodeSha,
start.VersionSet.ModelVersion,
start.VersionSet.ConfigVersion,
start.VersionSet.DatasetId,
start.VersionSet.ContractVersion,
start.InputHash,
start.TraceId,
start.RequestedAt
},
cancellationToken: cancellationToken));
return affected == 1;
}
public async Task HeartbeatAsync(Guid jobRunId, DateTimeOffset at, CancellationToken cancellationToken)
{
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
await connection.ExecuteAsync(new CommandDefinition(
HeartbeatSql,
new { JobRunId = jobRunId, At = at },
cancellationToken: cancellationToken));
}
public async Task CompleteAsync(JobRunCompletion completion, CancellationToken cancellationToken)
{
await using var connection = await connectionFactory.OpenAsync(cancellationToken);
var affected = await connection.ExecuteAsync(new CommandDefinition(
CompleteSql,
new
{
completion.JobRunId,
Status = completion.Status.ToString(),
completion.OutputHash,
completion.Watermark,
completion.ErrorCode,
completion.FinishedAt
},
cancellationToken: cancellationToken));
if (affected != 1)
{
throw new InvalidOperationException("Job run is absent or no longer Running.");
}
}
}
@@ -0,0 +1,17 @@
using System.Data.Common;
using Dapper;
namespace KArtSell.BuildingBlocks.Reliability;
public sealed class DapperOutboxWriter : IOutboxWriter
{
private const string Sql = """
insert into building_blocks.outbox_message
(message_id, event_type, schema_version, payload_json, correlation_id, occurred_at, payload_hash)
values (@MessageId, @EventType, @SchemaVersion, cast(@PayloadJson as jsonb), @CorrelationId, @OccurredAt, @PayloadHash)
on conflict (message_id) do nothing;
""";
public Task AddAsync(DbConnection connection, DbTransaction transaction, OutboxMessage message, CancellationToken cancellationToken)
=> connection.ExecuteAsync(new CommandDefinition(Sql, message, transaction, cancellationToken: cancellationToken));
}
@@ -0,0 +1,15 @@
using System.Data.Common;
namespace KArtSell.BuildingBlocks.Reliability;
public interface IInboxStore
{
Task<bool> TryBeginAsync(
DbConnection connection,
DbTransaction transaction,
string consumer,
Guid messageId,
string payloadHash,
DateTimeOffset receivedAt,
CancellationToken cancellationToken);
}
@@ -0,0 +1,8 @@
namespace KArtSell.BuildingBlocks.Reliability;
public interface IJobRunRepository
{
Task<bool> TryStartAsync(JobRunStart start, CancellationToken cancellationToken);
Task HeartbeatAsync(Guid jobRunId, DateTimeOffset at, CancellationToken cancellationToken);
Task CompleteAsync(JobRunCompletion completion, CancellationToken cancellationToken);
}
@@ -0,0 +1,8 @@
using System.Data.Common;
namespace KArtSell.BuildingBlocks.Reliability;
public interface IOutboxWriter
{
Task AddAsync(DbConnection connection, DbTransaction transaction, OutboxMessage message, CancellationToken cancellationToken);
}
@@ -0,0 +1,34 @@
using KArtSell.BuildingBlocks.Versioning;
namespace KArtSell.BuildingBlocks.Reliability;
public enum JobRunStatus
{
Requested,
Running,
Succeeded,
BusinessHold,
Quarantined,
Failed,
DeadLettered
}
public sealed record JobRunStart(
Guid JobRunId,
string JobType,
int JobVersion,
string ScopeKey,
string IdempotencyKey,
string? Watermark,
VersionSet VersionSet,
string? InputHash,
string TraceId,
DateTimeOffset RequestedAt);
public sealed record JobRunCompletion(
Guid JobRunId,
JobRunStatus Status,
string? OutputHash,
string? Watermark,
string? ErrorCode,
DateTimeOffset FinishedAt);
@@ -0,0 +1,10 @@
namespace KArtSell.BuildingBlocks.Reliability;
public sealed record OutboxMessage(
Guid MessageId,
string EventType,
int SchemaVersion,
string PayloadJson,
string CorrelationId,
DateTimeOffset OccurredAt,
string PayloadHash);
@@ -0,0 +1,11 @@
namespace KArtSell.BuildingBlocks.Time;
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
public sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
@@ -0,0 +1,32 @@
namespace KArtSell.BuildingBlocks.Versioning;
/// <summary>
/// Immutable identity of the data, model, configuration and code used for a decision.
/// Values are opaque, approved identifiers or SHA-256 digests; they are never inferred from the client.
/// </summary>
public sealed record VersionSet(
string DatasetId,
string DataHash,
string ModelVersion,
string ConfigVersion,
string CodeSha,
string ContractVersion)
{
public void EnsureValid()
{
EnsureNotBlank(DatasetId, nameof(DatasetId));
EnsureNotBlank(DataHash, nameof(DataHash));
EnsureNotBlank(ModelVersion, nameof(ModelVersion));
EnsureNotBlank(ConfigVersion, nameof(ConfigVersion));
EnsureNotBlank(CodeSha, nameof(CodeSha));
EnsureNotBlank(ContractVersion, nameof(ContractVersion));
}
private static void EnsureNotBlank(string value, string name)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException($"{name} must be an approved, non-blank identifier.", name);
}
}
}