From 6507ae47fb5c6e7943173fb1ad4345625dcd6bf6 Mon Sep 17 00:00:00 2001 From: "codebelt-aicia[bot]" Date: Sun, 18 Oct 2026 14:59:29 +0000 Subject: [PATCH 01/13] V0.5.13/service update --- .nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt | 6 ++++++ CHANGELOG.md | 4 ++++ Directory.Packages.props | 10 +++++----- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt b/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt index d7ad697..f247856 100644 --- a/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 0.5.13 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 0.5.12 Availability: .NET 10 and .NET 9 diff --git a/CHANGELOG.md b/CHANGELOG.md index b85dedb..186dd6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. +## [0.5.13] - 2026-10-18 + +This is a service update that focuses on package dependencies. + ## [0.5.12] - 2026-09-14 This is a patch release focused on test infrastructure modernization, build system improvements, and dependency upgrades to their latest compatible versions across all supported target frameworks. diff --git a/Directory.Packages.props b/Directory.Packages.props index 086755e..4c9225c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,14 +3,14 @@ true - - + + - - - + + + From a35b3c4d9a99fa4c486180a01584274385c0584d Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 17:58:49 +0200 Subject: [PATCH 02/13] =?UTF-8?q?=F0=9F=93=9D=20consolidate=20agent=20guid?= =?UTF-8?q?ance=20into=20AGENTS.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent test and documentation guidance now lives in AGENTS.md, so the deprecated Copilot instructions file is removed to keep a single source of truth. --- .bot/README.md | 2 +- .github/copilot-instructions.md | 632 -------------------------------- AGENTS.md | 630 ++++++++++++++++++++++++++++++- 3 files changed, 628 insertions(+), 636 deletions(-) delete mode 100644 .github/copilot-instructions.md diff --git a/.bot/README.md b/.bot/README.md index 2cfca89..f7e13f0 100644 --- a/.bot/README.md +++ b/.bot/README.md @@ -7,4 +7,4 @@ This folder is reserved for local-only AI working material such as: - design alternatives - temporary agent state -Keep this folder out of source control. Move only finalized, non-confidential guidance into `AGENTS.md` or `.github/copilot-instructions.md`. +Keep this folder out of source control. Move only finalized, non-confidential guidance into `AGENTS.md`. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 4cd6ee6..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,632 +0,0 @@ ---- -description: 'Writing Unit Tests' -applyTo: "**/*.{cs,csproj}" ---- - -# Writing Unit Tests -This document provides instructions for writing unit tests for a project/solution. Please follow these guidelines to ensure consistency and maintainability. - -## 1. Base Class - -**Always inherit from the `Test` base class** for all unit test classes. -This ensures consistent setup, teardown, and output handling across all tests. - -> Important: Do NOT add `using Xunit.Abstractions`. xUnit v3 no longer exposes that namespace; including it is incorrect and will cause compilation errors. Use the `Codebelt.Extensions.Xunit` Test base class and `using Xunit;` as shown in the examples below. If you need access to test output, rely on the Test base class (which accepts the appropriate output helper) rather than importing `Xunit.Abstractions`. - -```csharp -using Codebelt.Extensions.Xunit; -using Xunit; - -namespace Your.Namespace -{ - public class YourTestClass : Test - { - public YourTestClass(ITestOutputHelper output) : base(output) - { - } - - // Your tests here - } -} -``` - -## 2. Test Method Attributes - -- Use `[Fact]` for standard unit tests. -- Use `[Theory]` with `[InlineData]` or other data sources for parameterized tests. - -## 3. Naming Conventions - -- **Test classes**: End with `Test` (e.g., `DateSpanTest`). -- **Test methods**: Use descriptive names that state the expected behavior (e.g., `ShouldReturnTrue_WhenConditionIsMet`). - -## 4. Assertions - -- Use `Assert` methods from xUnit for all assertions. -- Prefer explicit and expressive assertions (e.g., `Assert.Equal`, `Assert.NotNull`, `Assert.Contains`). - -## 5. File and Namespace Organization - -- Place test files in the appropriate test project and folder structure. -- Use namespaces that mirror the source code structure. The namespace of a test file MUST match the namespace of the System Under Test (SUT). Do NOT append ".Tests", ".Benchmarks" or similar suffixes to the namespace. Only the assembly/project name should indicate that the file is a test/benchmark (for example: YourProject.Foo.Tests assembly, but namespace YourProject.Foo). - - Example: If the SUT class is declared as: - ```csharp - namespace YourProject.Foo.Bar - { - public class Zoo { /* ... */ } - } - ``` - then the corresponding unit test class must use the exact same namespace: - ```csharp - namespace YourProject.Foo.Bar - { - public class ZooTest : Test { /* ... */ } - } - ``` - - Do NOT use: - ```csharp - namespace YourProject.Foo.Bar.Tests { /* ... */ } // ❌ - namespace YourProject.Foo.Bar.Benchmarks { /* ... */ } // ❌ - ``` - - The unit tests for the YourProject.Foo assembly live in the YourProject.Foo.Tests assembly. - - The functional tests for the YourProject.Foo assembly live in the YourProject.Foo.FunctionalTests assembly. - - Test class names end with Test and live in the same namespace as the class being tested, e.g., the unit tests for the Boo class that resides in the YourProject.Foo assembly would be named BooTest and placed in the YourProject.Foo namespace in the YourProject.Foo.Tests assembly. - - Modify the associated .csproj file to override the root namespace so the compiled namespace matches the SUT. Example: - ```xml - - YourProject.Foo - - ``` -- When generating test scaffolding automatically, resolve the SUT's namespace from the source file (or project/assembly metadata) and use that exact namespace in the test file header. - -- Notes: - - This rule ensures type discovery and XML doc links behave consistently and reduces confusion when reading tests. - - Keep folder structure aligned with the production code layout to make locating SUT <-> test pairs straightforward. - -## 6. Example Test - -```csharp -using System; -using System.Globalization; -using Codebelt.Extensions.Xunit; -using Xunit; - -namespace YourProject -{ - /// - /// Tests for the class. - /// - public class DateSpanTest : Test - { - public DateSpanTest(ITestOutputHelper output) : base(output) - { - } - - [Fact] - public void Parse_ShouldGetOneMonthOfDifference_UsingIso8601String() - { - var start = new DateTime(2021, 3, 5).ToString("O"); - var end = new DateTime(2021, 4, 5).ToString("O"); - - var span = DateSpan.Parse(start, end); - - Assert.Equal("0:01:31:00:00:00.0", span.ToString()); - Assert.Equal(0, span.Years); - Assert.Equal(1, span.Months); - Assert.Equal(31, span.Days); - Assert.Equal(0, span.Hours); - Assert.Equal(0, span.Minutes); - Assert.Equal(0, span.Seconds); - Assert.Equal(0, span.Milliseconds); - - Assert.Equal(0.08493150684931507, span.TotalYears); - Assert.Equal(1, span.TotalMonths); - Assert.Equal(31, span.TotalDays); - Assert.Equal(744, span.TotalHours); - Assert.Equal(44640, span.TotalMinutes); - Assert.Equal(2678400, span.TotalSeconds); - Assert.Equal(2678400000, span.TotalMilliseconds); - - Assert.Equal(6, span.GetWeeks()); - Assert.Equal(-1566296493, span.GetHashCode()); - - TestOutput.WriteLine(span.ToString()); - } - } -} -``` - -## 7. Additional Guidelines - -- Keep tests focused and isolated. -- Do not rely on external systems except for xUnit itself and Codebelt.Extensions.Xunit (and derived from this). -- Ensure tests are deterministic and repeatable. - -## 8. Test Doubles - -- Preferred test doubles include dummies, fakes, stubs and spies if and when the design allows it. -- Under special circumstances, mock can be used (using Moq library). -- Before overriding methods, verify that the method is virtual or abstract; this rule also applies to mocks. -- Never mock IMarshaller; always use a new instance of JsonMarshaller. - -## 9. Avoid `InternalsVisibleTo` in Tests - -- **Do not** use `InternalsVisibleTo` to access internal types or members from test projects. -- Prefer **indirect testing via public APIs** that depend on the internal implementation (public facades, public extension methods, or other public entry points). - -### Preferred Pattern - -**Pattern name:** Public Facade Testing (also referred to as *Public API Proxy Testing*) - -**Description:** -Internal classes and methods must be validated by exercising the public API that consumes them. Tests should assert observable behavior exposed by the public surface rather than targeting internal implementation details directly. - -### Example Mapping - -- **Internal helper:** `DelimitedString` (internal static class) -- **Public API:** `TestOutputHelperExtensions.WriteLines()` (public extension method) -- **Test strategy:** Write tests for `WriteLines()` and verify its public behavior. The internal call to `DelimitedString.Create()` is exercised implicitly. - -### Benefits - -- Avoids exposing internal types to test assemblies. -- Ensures tests reflect real-world usage patterns. -- Maintains strong encapsulation and a clean public API. -- Tests remain resilient to internal refactoring as long as public behavior is preserved. - -### When to Apply - -- Internal logic is fully exercised through existing public APIs. -- Public entry points provide sufficient coverage of internal code paths. -- The internal implementation exists solely as a helper or utility for public-facing functionality. - -## 10. ExcludeFromCodeCoverage Prohibition - -**Do not use `ExcludeFromCodeCoverage` attribute on any code.** This includes: - -- Test classes or test methods -- Production code -- Configuration code -- Any other code path - -### Rationale - -- Excluding code from coverage hides gaps and creates false confidence in test completeness. -- If a code path cannot or should not be tested, refactor the code to eliminate that path rather than hiding it from metrics. -- Every executable line should be covered by tests or be genuinely unreachable (dead code to be removed). - -### Alternative Approaches - -- **Untestable code paths**: Refactor to separate concerns and eliminate the untestable path. -- **External dependencies**: Use test doubles (fakes, stubs, spies) instead of excluding from coverage. -- **Configuration-only code**: Move to configuration files or extract into testable methods. -- **Generated or third-party code**: These should not be in the primary codebase; use NuGet packages or dedicated vendor folders if necessary. - ---- -description: 'Writing Performance Tests' -applyTo: "tuning/**, **/*Benchmark*.cs" ---- - -# Writing Performance Tests -This document provides guidance for writing performance tests (benchmarks) for a project/solution using BenchmarkDotNet. Follow these guidelines to keep benchmarks consistent, readable, and comparable. - -## 1. Naming and Placement - -- Place micro- and component-benchmarks under the `tuning/` folder or in projects named `*.Benchmarks`. -- Place benchmark files in the appropriate benchmark project and folder structure. -- Use namespaces that mirror the source code structure, e.g. do not suffix with `Benchmarks`. -Namespace rule: DO NOT append `.Benchmarks` to the namespace. Benchmarks must live in the same namespace as the production assembly. Example: if the production assembly uses `namespace YourProject.Security.Cryptography`, the benchmark file should also use: - ``` - namespace YourProject.Security.Cryptography - { - public class Sha512256Benchmark { /* ... */ } - } - ``` -The class name must end with `Benchmark`, but the namespace must match the assembly (no `.Benchmarks` suffix). -- The benchmarks for the YourProject.Bar assembly live in the YourProject.Bar.Benchmarks assembly. -- Benchmark class names end with Benchmark and live in the same namespace as the class being measured, e.g., the benchmarks for the Zoo class that resides in the YourProject.Bar assembly would be named ZooBenchmark and placed in the YourProject.Bar namespace in the YourProject.Bar.Benchmarks assembly. -- Modify the associated .csproj file to override the root namespace, e.g., YourProject.Bar. - -## 2. Attributes and Configuration - -- Use `BenchmarkDotNet` attributes to express intent and collect relevant metrics: - - `[MemoryDiagnoser]` to capture memory allocations. - - `[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]` to group related benchmarks. - - `[Params]` for input sizes or variations to exercise multiple scenarios. - - `[GlobalSetup]` for one-time initialization that's not part of measured work. - - `[Benchmark]` on methods representing measured operations; consider `Baseline = true` and `Description` to improve report clarity. -- Keep benchmark configuration minimal and explicit; prefer in-class attributes over large shared configs unless re-used widely. - -## 3. Structure and Best Practices - -- Keep benchmarks focused: each `Benchmark` method should measure a single logical operation. -- Avoid doing expensive setup work inside a measured method; use `[GlobalSetup]`, `[IterationSetup]`, or cached fields instead. -- Use `Params` to cover micro, mid and macro input sizes (for example: small, medium, large) and verify performance trends across them. -- Use small, deterministic data sets and avoid external systems (network, disk, DB). If external systems are necessary, mark them clearly and do not include them in CI benchmark runs by default. -- Capture results that are meaningful: time, allocations, and if needed custom counters. Prefer `MemoryDiagnoser` and descriptive `Description` values. - -## 4. Naming Conventions for Methods - -- Method names should be descriptive and indicate the scenario, e.g., `Parse_Short`, `ComputeHash_Large`. -- When comparing implementations, mark one method with `Baseline = true` and use similar names so reports are easy to read. - -## 5. Example Benchmark - -```csharp -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; - -namespace YourProject -{ - [MemoryDiagnoser] - [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] - public class SampleOperationBenchmark - { - [Params(8, 256, 4096)] - public int Count { get; set; } - - private byte[] _payload; - - [GlobalSetup] - public void Setup() - { - _payload = new byte[Count]; - // deterministic initialization - } - - [Benchmark(Baseline = true, Description = "Operation - baseline")] - public int Operation_Baseline() => SampleOperation.Process(_payload); - - [Benchmark(Description = "Operation - optimized")] - public int Operation_Optimized() => SampleOperation.ProcessOptimized(_payload); - } -} -``` - -## 6. Reporting and CI - -- Benchmarks are primarily for local and tuning runs; be cautious about running heavy BenchmarkDotNet workloads in CI. Prefer targeted runs or harnesses for CI where appropriate. -- Keep benchmark projects isolated (e.g., `tuning/*.csproj`) so they don't affect package builds or production artifacts. - -## 7. Additional Guidelines - -- Keep benchmarks readable and well-documented; add comments explaining non-obvious choices. -- If a benchmark exposes regressions or optimizations, add a short note in the benchmark file referencing the relevant issue or PR. -- For any shared helpers for benchmarking, prefer small utility classes inside the `tuning` projects rather than cross-cutting changes to production code. - -For further examples, refer to the benchmark files under the `tuning/` folder. - ---- -description: 'Writing XML documentation' -applyTo: "**/*.cs" ---- - -# Writing XML documentation -This document provides instructions for writing XML documentation. - -## 1. Documentation Style - -- Use the same documentation style as found throughout the codebase. -- Add XML doc comments to public and protected classes and methods where appropriate. -- Example: - -```csharp -using System; -using System.Collections.Generic; -using System.IO; -using Cuemon.Collections.Generic; -using Cuemon.Configuration; -using Cuemon.IO; -using Cuemon.Text; - -namespace Cuemon.Security -{ - /// - /// Represents the base class from which all implementations of hash algorithms and checksums should derive. - /// - /// The type of the configured options. - /// - /// - /// - public abstract class Hash : Hash, IConfigurable where TOptions : ConvertibleOptions, new() - { - /// - /// Initializes a new instance of the class. - /// - /// The which may be configured. - protected Hash(Action setup) - { - Options = Patterns.Configure(setup); - } - - /// - /// Gets the configured options of this instance. - /// - /// The configured options of this instance. - public TOptions Options { get; } - - - /// - /// The endian-initializer of this instance. - /// - /// An instance of the configured options. - protected sealed override void EndianInitializer(EndianOptions options) - { - options.ByteOrder = Options.ByteOrder; - } - } - - /// - /// Represents the base class that defines the public facing structure to expose. - /// - /// - public abstract class Hash : IHash - { - /// - /// Initializes a new instance of the class. - /// - protected Hash() - { - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(bool input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(byte input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(char input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(DateTime input) - { - return ComputeHash(Convertible.GetBytes(input)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(DBNull input) - { - return ComputeHash(Convertible.GetBytes(input)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(decimal input) - { - return ComputeHash(Convertible.GetBytes(input)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(double input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(short input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(int input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(long input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(sbyte input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(float input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(ushort input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(uint input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(ulong input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// The which may be configured. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(string input, Action setup = null) - { - return ComputeHash(Convertible.GetBytes(input, setup)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(Enum input) - { - return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(params IConvertible[] input) - { - return ComputeHash(Arguments.ToEnumerableOf(input)); - } - - /// - /// Computes the hash value for the specified sequence of . - /// - /// The sequence of to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(IEnumerable input) - { - return ComputeHash(Convertible.GetBytes(input)); - } - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public abstract HashResult ComputeHash(byte[] input); - - /// - /// Computes the hash value for the specified . - /// - /// The to compute the hash code for. - /// A containing the computed hash code of the specified . - public virtual HashResult ComputeHash(Stream input) - { - return ComputeHash(Patterns.SafeInvoke(() => new MemoryStream(), destination => - { - Decorator.Enclose(input).CopyStream(destination); - return destination; - }).ToArray()); - } - - /// - /// Defines the initializer that must implement. - /// - /// An instance of the configured options. - protected abstract void EndianInitializer(EndianOptions options); - } -} - -namespace Cuemon.Security -{ - /// - /// Configuration options for . - /// - public class FowlerNollVoOptions : ConvertibleOptions - { - /// - /// Initializes a new instance of the class. - /// - /// - /// The following table shows the initial property values for an instance of . - /// - /// - /// Property - /// Initial Value - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public FowlerNollVoOptions() - { - Algorithm = FowlerNollVoAlgorithm.Fnv1a; - ByteOrder = Endianness.BigEndian; - } - - /// - /// Gets or sets the algorithm of the Fowler-Noll-Vo hash function. - /// - /// The algorithm of the Fowler-Noll-Vo hash function. - public FowlerNollVoAlgorithm Algorithm { get; set; } - } -} -``` diff --git a/AGENTS.md b/AGENTS.md index ffe7447..0d7aa61 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ Codebelt.SharedKernel is a foundational .NET library providing domain-driven des - `Codebelt.SharedKernel.Tests/` — Unit tests for all core types - `.nuget/` — Per-package NuGet metadata (icon, README, release notes) - `.docfx/` — DocFX documentation configuration -- `.github/` — CI/CD workflows, contributing guidelines, Copilot instructions +- `.github/` — CI/CD workflows, contributing guidelines, agent guidance ## Test Conventions @@ -41,7 +41,7 @@ Codebelt.SharedKernel is a foundational .NET library providing domain-driven des - Use `Microsoft.Testing.Platform` as the test runner (`UseMicrosoftTestingPlatformRunner=true`) - All tests are executable (`OutputType=Exe`) - Test namespaces must match the SUT namespace (System Under Test), without `.Tests` suffix -- See `.github/copilot-instructions.md` for detailed test writing guidelines +- See the Writing Unit Tests section in `AGENTS.md` for detailed test writing guidelines ## Build & CI @@ -56,7 +56,7 @@ Codebelt.SharedKernel is a foundational .NET library providing domain-driven des If a `.bot/` folder exists at the root, it contains **confidential, local-only** working material for AI agents — product requirement documents (PRDs), design proposals, agentic loop state, and brainstorming outputs. This folder is gitignored and never committed. -When starting creative or design work (new features, architecture decisions, PRD drafts), use the [brainstorming skill](https://skills.sh/obra/superpowers/brainstorming) and save outputs to `.bot/`. Only move finalized, non-confidential instructions into `AGENTS.md` or `.github/copilot-instructions.md`. +When starting creative or design work (new features, architecture decisions, PRD drafts), use the [brainstorming skill](https://skills.sh/obra/superpowers/brainstorming) and save outputs to `.bot/`. Only move finalized, non-confidential instructions into `AGENTS.md`. ## Git Operations Safeguards @@ -126,3 +126,627 @@ The final DocFX verification must run outside the working tree when possible. Th If a command cannot be run, report the exact limitation or failure instead of claiming the documentation was verified. + +Scope: **/*.{cs,csproj}. Guidance: Writing Unit Tests. + +# Writing Unit Tests +This document provides instructions for writing unit tests for a project/solution. Please follow these guidelines to ensure consistency and maintainability. + +## 1. Base Class + +**Always inherit from the `Test` base class** for all unit test classes. +This ensures consistent setup, teardown, and output handling across all tests. + +> Important: Do NOT add `using Xunit.Abstractions`. xUnit v3 no longer exposes that namespace; including it is incorrect and will cause compilation errors. Use the `Codebelt.Extensions.Xunit` Test base class and `using Xunit;` as shown in the examples below. If you need access to test output, rely on the Test base class (which accepts the appropriate output helper) rather than importing `Xunit.Abstractions`. + +```csharp +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Your.Namespace +{ + public class YourTestClass : Test + { + public YourTestClass(ITestOutputHelper output) : base(output) + { + } + + // Your tests here + } +} +``` + +## 2. Test Method Attributes + +- Use `[Fact]` for standard unit tests. +- Use `[Theory]` with `[InlineData]` or other data sources for parameterized tests. + +## 3. Naming Conventions + +- **Test classes**: End with `Test` (e.g., `DateSpanTest`). +- **Test methods**: Use descriptive names that state the expected behavior (e.g., `ShouldReturnTrue_WhenConditionIsMet`). + +## 4. Assertions + +- Use `Assert` methods from xUnit for all assertions. +- Prefer explicit and expressive assertions (e.g., `Assert.Equal`, `Assert.NotNull`, `Assert.Contains`). + +## 5. File and Namespace Organization + +- Place test files in the appropriate test project and folder structure. +- Use namespaces that mirror the source code structure. The namespace of a test file MUST match the namespace of the System Under Test (SUT). Do NOT append ".Tests", ".Benchmarks" or similar suffixes to the namespace. Only the assembly/project name should indicate that the file is a test/benchmark (for example: YourProject.Foo.Tests assembly, but namespace YourProject.Foo). + - Example: If the SUT class is declared as: + ```csharp + namespace YourProject.Foo.Bar + { + public class Zoo { /* ... */ } + } + ``` + then the corresponding unit test class must use the exact same namespace: + ```csharp + namespace YourProject.Foo.Bar + { + public class ZooTest : Test { /* ... */ } + } + ``` + - Do NOT use: + ```csharp + namespace YourProject.Foo.Bar.Tests { /* ... */ } // ❌ + namespace YourProject.Foo.Bar.Benchmarks { /* ... */ } // ❌ + ``` + - The unit tests for the YourProject.Foo assembly live in the YourProject.Foo.Tests assembly. + - The functional tests for the YourProject.Foo assembly live in the YourProject.Foo.FunctionalTests assembly. + - Test class names end with Test and live in the same namespace as the class being tested, e.g., the unit tests for the Boo class that resides in the YourProject.Foo assembly would be named BooTest and placed in the YourProject.Foo namespace in the YourProject.Foo.Tests assembly. + - Modify the associated .csproj file to override the root namespace so the compiled namespace matches the SUT. Example: + ```xml + + YourProject.Foo + + ``` +- When generating test scaffolding automatically, resolve the SUT's namespace from the source file (or project/assembly metadata) and use that exact namespace in the test file header. + +- Notes: + - This rule ensures type discovery and XML doc links behave consistently and reduces confusion when reading tests. + - Keep folder structure aligned with the production code layout to make locating SUT <-> test pairs straightforward. + +## 6. Example Test + +```csharp +using System; +using System.Globalization; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace YourProject +{ + /// + /// Tests for the class. + /// + public class DateSpanTest : Test + { + public DateSpanTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Parse_ShouldGetOneMonthOfDifference_UsingIso8601String() + { + var start = new DateTime(2021, 3, 5).ToString("O"); + var end = new DateTime(2021, 4, 5).ToString("O"); + + var span = DateSpan.Parse(start, end); + + Assert.Equal("0:01:31:00:00:00.0", span.ToString()); + Assert.Equal(0, span.Years); + Assert.Equal(1, span.Months); + Assert.Equal(31, span.Days); + Assert.Equal(0, span.Hours); + Assert.Equal(0, span.Minutes); + Assert.Equal(0, span.Seconds); + Assert.Equal(0, span.Milliseconds); + + Assert.Equal(0.08493150684931507, span.TotalYears); + Assert.Equal(1, span.TotalMonths); + Assert.Equal(31, span.TotalDays); + Assert.Equal(744, span.TotalHours); + Assert.Equal(44640, span.TotalMinutes); + Assert.Equal(2678400, span.TotalSeconds); + Assert.Equal(2678400000, span.TotalMilliseconds); + + Assert.Equal(6, span.GetWeeks()); + Assert.Equal(-1566296493, span.GetHashCode()); + + TestOutput.WriteLine(span.ToString()); + } + } +} +``` + +## 7. Additional Guidelines + +- Keep tests focused and isolated. +- Do not rely on external systems except for xUnit itself and Codebelt.Extensions.Xunit (and derived from this). +- Ensure tests are deterministic and repeatable. + +## 8. Test Doubles + +- Preferred test doubles include dummies, fakes, stubs and spies if and when the design allows it. +- Under special circumstances, mock can be used (using Moq library). +- Before overriding methods, verify that the method is virtual or abstract; this rule also applies to mocks. +- Never mock IMarshaller; always use a new instance of JsonMarshaller. + +## 9. Avoid `InternalsVisibleTo` in Tests + +- **Do not** use `InternalsVisibleTo` to access internal types or members from test projects. +- Prefer **indirect testing via public APIs** that depend on the internal implementation (public facades, public extension methods, or other public entry points). + +### Preferred Pattern + +**Pattern name:** Public Facade Testing (also referred to as *Public API Proxy Testing*) + +**Description:** +Internal classes and methods must be validated by exercising the public API that consumes them. Tests should assert observable behavior exposed by the public surface rather than targeting internal implementation details directly. + +### Example Mapping + +- **Internal helper:** `DelimitedString` (internal static class) +- **Public API:** `TestOutputHelperExtensions.WriteLines()` (public extension method) +- **Test strategy:** Write tests for `WriteLines()` and verify its public behavior. The internal call to `DelimitedString.Create()` is exercised implicitly. + +### Benefits + +- Avoids exposing internal types to test assemblies. +- Ensures tests reflect real-world usage patterns. +- Maintains strong encapsulation and a clean public API. +- Tests remain resilient to internal refactoring as long as public behavior is preserved. + +### When to Apply + +- Internal logic is fully exercised through existing public APIs. +- Public entry points provide sufficient coverage of internal code paths. +- The internal implementation exists solely as a helper or utility for public-facing functionality. + +## 10. ExcludeFromCodeCoverage Prohibition + +**Do not use `ExcludeFromCodeCoverage` attribute on any code.** This includes: + +- Test classes or test methods +- Production code +- Configuration code +- Any other code path + +### Rationale + +- Excluding code from coverage hides gaps and creates false confidence in test completeness. +- If a code path cannot or should not be tested, refactor the code to eliminate that path rather than hiding it from metrics. +- Every executable line should be covered by tests or be genuinely unreachable (dead code to be removed). + +### Alternative Approaches + +- **Untestable code paths**: Refactor to separate concerns and eliminate the untestable path. +- **External dependencies**: Use test doubles (fakes, stubs, spies) instead of excluding from coverage. +- **Configuration-only code**: Move to configuration files or extract into testable methods. +- **Generated or third-party code**: These should not be in the primary codebase; use NuGet packages or dedicated vendor folders if necessary. + +Scope: tuning/**, **/*Benchmark*.cs. Guidance: Writing Performance Tests. + +# Writing Performance Tests +This document provides guidance for writing performance tests (benchmarks) for a project/solution using BenchmarkDotNet. Follow these guidelines to keep benchmarks consistent, readable, and comparable. + +## 1. Naming and Placement + +- Place micro- and component-benchmarks under the `tuning/` folder or in projects named `*.Benchmarks`. +- Place benchmark files in the appropriate benchmark project and folder structure. +- Use namespaces that mirror the source code structure, e.g. do not suffix with `Benchmarks`. +Namespace rule: DO NOT append `.Benchmarks` to the namespace. Benchmarks must live in the same namespace as the production assembly. Example: if the production assembly uses `namespace YourProject.Security.Cryptography`, the benchmark file should also use: + ``` + namespace YourProject.Security.Cryptography + { + public class Sha512256Benchmark { /* ... */ } + } + ``` +The class name must end with `Benchmark`, but the namespace must match the assembly (no `.Benchmarks` suffix). +- The benchmarks for the YourProject.Bar assembly live in the YourProject.Bar.Benchmarks assembly. +- Benchmark class names end with Benchmark and live in the same namespace as the class being measured, e.g., the benchmarks for the Zoo class that resides in the YourProject.Bar assembly would be named ZooBenchmark and placed in the YourProject.Bar namespace in the YourProject.Bar.Benchmarks assembly. +- Modify the associated .csproj file to override the root namespace, e.g., YourProject.Bar. + +## 2. Attributes and Configuration + +- Use `BenchmarkDotNet` attributes to express intent and collect relevant metrics: + - `[MemoryDiagnoser]` to capture memory allocations. + - `[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]` to group related benchmarks. + - `[Params]` for input sizes or variations to exercise multiple scenarios. + - `[GlobalSetup]` for one-time initialization that's not part of measured work. + - `[Benchmark]` on methods representing measured operations; consider `Baseline = true` and `Description` to improve report clarity. +- Keep benchmark configuration minimal and explicit; prefer in-class attributes over large shared configs unless re-used widely. + +## 3. Structure and Best Practices + +- Keep benchmarks focused: each `Benchmark` method should measure a single logical operation. +- Avoid doing expensive setup work inside a measured method; use `[GlobalSetup]`, `[IterationSetup]`, or cached fields instead. +- Use `Params` to cover micro, mid and macro input sizes (for example: small, medium, large) and verify performance trends across them. +- Use small, deterministic data sets and avoid external systems (network, disk, DB). If external systems are necessary, mark them clearly and do not include them in CI benchmark runs by default. +- Capture results that are meaningful: time, allocations, and if needed custom counters. Prefer `MemoryDiagnoser` and descriptive `Description` values. + +## 4. Naming Conventions for Methods + +- Method names should be descriptive and indicate the scenario, e.g., `Parse_Short`, `ComputeHash_Large`. +- When comparing implementations, mark one method with `Baseline = true` and use similar names so reports are easy to read. + +## 5. Example Benchmark + +```csharp +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace YourProject +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class SampleOperationBenchmark + { + [Params(8, 256, 4096)] + public int Count { get; set; } + + private byte[] _payload; + + [GlobalSetup] + public void Setup() + { + _payload = new byte[Count]; + // deterministic initialization + } + + [Benchmark(Baseline = true, Description = "Operation - baseline")] + public int Operation_Baseline() => SampleOperation.Process(_payload); + + [Benchmark(Description = "Operation - optimized")] + public int Operation_Optimized() => SampleOperation.ProcessOptimized(_payload); + } +} +``` + +## 6. Reporting and CI + +- Benchmarks are primarily for local and tuning runs; be cautious about running heavy BenchmarkDotNet workloads in CI. Prefer targeted runs or harnesses for CI where appropriate. +- Keep benchmark projects isolated (e.g., `tuning/*.csproj`) so they don't affect package builds or production artifacts. + +## 7. Additional Guidelines + +- Keep benchmarks readable and well-documented; add comments explaining non-obvious choices. +- If a benchmark exposes regressions or optimizations, add a short note in the benchmark file referencing the relevant issue or PR. +- For any shared helpers for benchmarking, prefer small utility classes inside the `tuning` projects rather than cross-cutting changes to production code. + +For further examples, refer to the benchmark files under the `tuning/` folder. + +Scope: **/*.cs. Guidance: Writing XML documentation. + +# Writing XML documentation +This document provides instructions for writing XML documentation. + +## 1. Documentation Style + +- Use the same documentation style as found throughout the codebase. +- Add XML doc comments to public and protected classes and methods where appropriate. +- Example: + +```csharp +using System; +using System.Collections.Generic; +using System.IO; +using Cuemon.Collections.Generic; +using Cuemon.Configuration; +using Cuemon.IO; +using Cuemon.Text; + +namespace Cuemon.Security +{ + /// + /// Represents the base class from which all implementations of hash algorithms and checksums should derive. + /// + /// The type of the configured options. + /// + /// + /// + public abstract class Hash : Hash, IConfigurable where TOptions : ConvertibleOptions, new() + { + /// + /// Initializes a new instance of the class. + /// + /// The which may be configured. + protected Hash(Action setup) + { + Options = Patterns.Configure(setup); + } + + /// + /// Gets the configured options of this instance. + /// + /// The configured options of this instance. + public TOptions Options { get; } + + + /// + /// The endian-initializer of this instance. + /// + /// An instance of the configured options. + protected sealed override void EndianInitializer(EndianOptions options) + { + options.ByteOrder = Options.ByteOrder; + } + } + + /// + /// Represents the base class that defines the public facing structure to expose. + /// + /// + public abstract class Hash : IHash + { + /// + /// Initializes a new instance of the class. + /// + protected Hash() + { + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(bool input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(byte input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(char input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(DateTime input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(DBNull input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(decimal input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(double input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(short input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(int input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(long input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(sbyte input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(float input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(ushort input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(uint input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(ulong input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// The which may be configured. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(string input, Action setup = null) + { + return ComputeHash(Convertible.GetBytes(input, setup)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(Enum input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(params IConvertible[] input) + { + return ComputeHash(Arguments.ToEnumerableOf(input)); + } + + /// + /// Computes the hash value for the specified sequence of . + /// + /// The sequence of to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(IEnumerable input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public abstract HashResult ComputeHash(byte[] input); + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(Stream input) + { + return ComputeHash(Patterns.SafeInvoke(() => new MemoryStream(), destination => + { + Decorator.Enclose(input).CopyStream(destination); + return destination; + }).ToArray()); + } + + /// + /// Defines the initializer that must implement. + /// + /// An instance of the configured options. + protected abstract void EndianInitializer(EndianOptions options); + } +} + +namespace Cuemon.Security +{ + /// + /// Configuration options for . + /// + public class FowlerNollVoOptions : ConvertibleOptions + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The following table shows the initial property values for an instance of . + /// + /// + /// Property + /// Initial Value + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public FowlerNollVoOptions() + { + Algorithm = FowlerNollVoAlgorithm.Fnv1a; + ByteOrder = Endianness.BigEndian; + } + + /// + /// Gets or sets the algorithm of the Fowler-Noll-Vo hash function. + /// + /// The algorithm of the Fowler-Noll-Vo hash function. + public FowlerNollVoAlgorithm Algorithm { get; set; } + } +} +``` From ea8a5d213f9c568f3d262f9a602c8920eed915ed Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 17:58:49 +0200 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=93=9D=20correct=20dotnet=20test=20?= =?UTF-8?q?command=20in=20contributing=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test loop used an invalid dotnet test argument form; the project path is now passed with --project so contributors can run each test project. --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8b09bf1..bf30585 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -35,7 +35,7 @@ Run tests one project at a time so a failing or hanging project is attributable. ```powershell $testProjects = Get-ChildItem test -Filter *.csproj -Recurse foreach ($project in $testProjects) { - dotnet test $project.FullName --configuration Release --no-restore + dotnet test --project $project.FullName --configuration Release --no-restore } ``` From 06a9c4428dfb86e2ae15bdef3e42c85705b5b2d2 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 17:58:49 +0200 Subject: [PATCH 04/13] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20upgrade=20xunit=20an?= =?UTF-8?q?d=20swap=20coverage=20test=20packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test tooling moves to the latest stable xunit v3 and Test SDK releases, and coverage collection switches to Codebelt.Coverlet.MTP with the hang dump extension for Microsoft.Testing.Platform. --- Directory.Build.props | 3 ++- Directory.Packages.props | 11 ++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index e711169..8c05d58 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -70,7 +70,8 @@ - + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 4c9225c..fcc583d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,16 +3,17 @@ true + - - + + - - - + + + \ No newline at end of file From 1438b94fc38b9cfddf160b8b105d641847f9e052 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 17:58:50 +0200 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=90=B3=20serve=20docfx=20site=20thr?= =?UTF-8?q?ough=20dhi=20nginx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docfx image now copies the generated site into a DHI nginx container with a dedicated config for fingerprinted asset caching and stable page URLs. --- .docfx/Dockerfile.docfx | 11 +++++------ .docfx/nginx.conf | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 .docfx/nginx.conf diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx index 7d97dde..e29eda1 100644 --- a/.docfx/Dockerfile.docfx +++ b/.docfx/Dockerfile.docfx @@ -1,17 +1,16 @@ -ARG NGINX_VERSION=1.31-alpine +ARG NGINX_VERSION=1.31-alpine -FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base +FROM --platform=$BUILDPLATFORM dhi.io/nginx:${NGINX_VERSION} AS base RUN rm -rf /usr/share/nginx/html/* -FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2.78.5 AS build +FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2 AS build ADD [".", "docfx"] RUN cd docfx; \ docfx build -FROM nginx:${NGINX_VERSION} AS final +FROM dhi.io/nginx:${NGINX_VERSION} AS final WORKDIR /usr/share/nginx/html COPY --from=build /build/docfx/wwwroot /usr/share/nginx/html - -ENTRYPOINT ["nginx", "-g", "daemon off;"] +COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/.docfx/nginx.conf b/.docfx/nginx.conf new file mode 100644 index 0000000..c95d1b1 --- /dev/null +++ b/.docfx/nginx.conf @@ -0,0 +1,31 @@ +server { + listen 8080; + server_name _; + # The ingress terminates TLS; keep redirects from exposing this container port. + absolute_redirect off; + + root /usr/share/nginx/html; + index index.html; + + # DocFX fingerprints these generated dependencies in their filenames. They can be + # cached permanently because changed content is published under a different URL. + location ~ "^/public/.+-[A-Z0-9]{8}(?:-[A-Z0-9]{8})?(?:\.min)?\.(?:(?:css|js)(?:\.map)?|woff2?)$" { + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable" always; + add_header Vary "Accept-Encoding" always; + } + + # Pages, navigation data, images, and DocFX entry assets keep stable URLs. Do not + # retain them across deployments, and ignore validators cached under an old policy. + location / { + # Nginx derives ETags from modification time and file size. Those values can be + # reused by files copied from different image layers, producing a false 304. + etag off; + if_modified_since off; + try_files $uri $uri/ =404; + add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always; + add_header Expires "0" always; + add_header Pragma "no-cache" always; + add_header Vary "Accept-Encoding" always; + } +} From 1f11caef9f29363dafb8841981b58e3916ca622d Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 17:58:50 +0200 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=94=A7=20configure=20docfx=20member?= =?UTF-8?q?=20layout=20and=20sitemap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Member pages are split out per member, the shared ms-style template is applied, and a sitemap is generated for the published site. --- .docfx/docfx.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.docfx/docfx.json b/.docfx/docfx.json index 60b9142..8fdf733 100644 --- a/.docfx/docfx.json +++ b/.docfx/docfx.json @@ -10,6 +10,7 @@ } ], "dest": "api", + "memberLayout": "separatePages", "filter": "filterConfig.yml", "properties": { "TargetFramework": "net10.0" @@ -63,7 +64,8 @@ "template": [ "default", "modern", - "templates/shared-kernel" + "templates/shared-kernel", + "templates/ms-style" ], "overwrite": [ { @@ -83,6 +85,10 @@ "keepFileLink": false, "cleanupCacheHistory": false, "disableGitFeatures": false, + "sitemap": { + "baseUrl": "https://sharedkernel.codebelt.net/", + "changefreq": "monthly" + }, "xrefService": [ "https://xref.docs.microsoft.com/query?uid={uid}" ] } From cc556cd646c1e056f17bb3aeeb5debd9c168ee14 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 17:58:50 +0200 Subject: [PATCH 07/13] =?UTF-8?q?=F0=9F=91=B7=20derive=20service=20update?= =?UTF-8?q?=20version=20from=20git=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow now resolves the next patch version from the latest stable Git tag instead of the changelog heading, and reuses a pending TBD release entry. --- .github/workflows/service-update.yml | 93 ++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/.github/workflows/service-update.yml b/.github/workflows/service-update.yml index f9a235e..1f69586 100644 --- a/.github/workflows/service-update.yml +++ b/.github/workflows/service-update.yml @@ -31,6 +31,7 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + fetch-tags: true - name: Resolve trigger inputs id: trigger @@ -42,13 +43,68 @@ jobs: - name: Determine new version for this repo id: newver + shell: bash run: | - CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1) - NEW=$(echo "$CURRENT" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}') - BRANCH="v${NEW}/service-update" - echo "current=$CURRENT" >> $GITHUB_OUTPUT - echo "new=$NEW" >> $GITHUB_OUTPUT - echo "branch=$BRANCH" >> $GITHUB_OUTPUT + python3 - <<'PY' + import os + import re + import subprocess + import sys + + tag_pattern = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$") + tags = subprocess.check_output(["git", "tag", "--list"], text=True).splitlines() + stable_tags = [] + for tag in tags: + match = tag_pattern.fullmatch(tag) + if match: + version = tuple(int(part) for part in match.groups()) + stable_tags.append((version, tag)) + + if not stable_tags: + print("::error::No stable vMAJOR.MINOR.PATCH Git tag was found.") + sys.exit(1) + + _, latest_tag = max(stable_tags, key=lambda item: item[0]) + latest_version = latest_tag[1:] + major, minor, patch = (int(part) for part in latest_version.split(".")) + next_version = f"{major}.{minor}.{patch + 1}" + + with open("CHANGELOG.md", encoding="utf-8") as changelog_file: + changelog = changelog_file.read() + + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - ([^\r\n]+)[ \t]*$", changelog) + if not heading: + print("::error::CHANGELOG.md has no versioned heading to compare with Git tags.") + sys.exit(1) + + current_version, current_status = heading.groups() + current_status = current_status.strip() + if current_status.upper() != "TBD" and not re.fullmatch(r"\d{4}-\d{2}-\d{2}", current_status): + print(f"::error::Unsupported CHANGELOG.md version status: {current_status}") + sys.exit(1) + + if current_version == next_version and current_status.upper() == "TBD": + # Reuse an already pending next release, such as [5.0.13] - TBD. + new_version = next_version + elif current_version == latest_version and current_status.upper() != "TBD": + # The changelog is at the latest released tag; start the next patch release. + new_version = next_version + else: + print( + f"::error::CHANGELOG.md starts at {current_version} ({current_status}), " + f"but the latest stable Git tag is {latest_tag}. Expected either " + f"[{latest_version}] with a release date or [{next_version}] - TBD." + ) + sys.exit(1) + + branch = f"v{new_version}/service-update" + print(f"Latest stable Git tag: {latest_tag}") + print(f"Latest CHANGELOG.md version: {current_version} ({current_status})") + print(f"Service update version: {new_version}") + with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write(f"current={current_version}\n") + output.write(f"new={new_version}\n") + output.write(f"branch={branch}\n") - name: Generate codebelt-aicia token id: app-token @@ -81,14 +137,27 @@ jobs: from datetime import date new_ver = os.environ['NEW_VERSION'] today = date.today().isoformat() - entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n" - with open("CHANGELOG.md") as f: + service_note = "This release also includes a service update focused on package dependencies." + with open("CHANGELOG.md", encoding="utf-8") as f: content = f.read() - idx = content.find("## [") - content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) - with open("CHANGELOG.md", "w") as f: + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - (TBD|\d{4}-\d{2}-\d{2})[ \t]*$", content) + if heading and heading.group(1) == new_ver: + if heading.group(2) == "TBD": + content = content[:heading.start(2)] + today + content[heading.end(2):] + heading = re.search(r"(?m)^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}[ \t]*$", content) + section_end = re.search(r"(?m)^## \[", content[heading.end():]) + end = heading.end() + section_end.start() if section_end else len(content) + if service_note not in content[heading.end():end]: + remainder = content[heading.end():] + content = content[:heading.end()] + "\n\n" + service_note + "\n\n" + remainder.lstrip("\r\n") + print(f"Updated existing CHANGELOG entry for v{new_ver}") + else: + entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n" + idx = content.find("## [") + content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) + print(f"Added CHANGELOG entry for v{new_ver}") + with open("CHANGELOG.md", "w", encoding="utf-8") as f: f.write(content) - print(f"CHANGELOG updated for v{new_ver}") EOF env: NEW_VERSION: ${{ steps.newver.outputs.new }} From 285ba8572062c93cfbcbb635cd63f964484d2e99 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 17:58:50 +0200 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=92=AC=20update=20changelog=20for?= =?UTF-8?q?=200.5.13=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summarizes the 0.5.13 service update for readers of the repository, covering dependency upgrades, DocFX hosting, guidance consolidation, and automation changes. --- CHANGELOG.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 186dd6e..18676ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,26 @@ For more details, please refer to `PackageReleaseNotes.txt` on a per assembly ba ## [0.5.13] - 2026-10-18 -This is a service update that focuses on package dependencies. +This is a patch release focused on dependency maintenance, test diagnostics, documentation hosting, and repository guidance. + +### Added + +- `Codebelt.Coverlet.MTP` 10.1.0 and `Microsoft.Testing.Extensions.HangDump` 2.5.1 as shared test dependencies, +- Nginx configuration that listens on port 8080, caches fingerprinted DocFX assets, and disables caching for stable page URLs. + +### Changed + +- Upgraded `Cuemon.Extensions.IO` from 10.7.1 to 10.8.1, `Codebelt.Extensions.Xunit.App` from 12.0.1 to 12.0.2, and `Savvyio.Domain`, `Savvyio.Extensions.Newtonsoft.Json`, and `Savvyio.Extensions.Text.Json` from 5.0.12 to 5.0.13, +- Upgraded `Microsoft.NET.Test.Sdk` from 18.10.0 to 18.11.0, `xunit.v3` and `xunit.v3.runner.console` from 4.0.1 to 4.0.2, and `xunit.runner.visualstudio` from 4.0.0 to 4.0.1, +- Consolidated unit-test, benchmark, and XML documentation guidance into `AGENTS.md`, preserving examples and explicit file scopes, +- Updated DocFX hosting to DHI nginx and the `codebeltnet/docfx:2` image; configured separate member pages, the shared style template, and sitemap metadata, +- Updated contributor test commands for the Microsoft.Testing.Platform runner, +- Updated service-update automation to resolve the next patch from stable Git tags, reuse pending changelog entries, and preserve their content. + +### Removed + +- Direct test dependency on `Microsoft.Testing.Extensions.CodeCoverage`, +- Deprecated `.github/copilot-instructions.md`; its guidance is maintained in `AGENTS.md`. ## [0.5.12] - 2026-09-14 @@ -202,6 +221,7 @@ Purely an ALM release. No changes to the codebase. - CoordinatedUniversalTime record in the Codebelt.SharedKernel namespace that represents an object that can be used when you need a timestamp that is based on an absolute time (UTC) - TimeToLive record in the Codebelt.SharedKernel namespace that represents an object that can be used when issuing authentication tokens or similar (TTL) +[0.5.13]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.12...v0.5.13 [0.5.12]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.11...v0.5.12 [0.5.11]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.10...v0.5.11 [0.5.10]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.9...v0.5.10 From 477ce1d5ca115176545d29cfef66e660ec58b129 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 18 Oct 2026 18:48:47 +0200 Subject: [PATCH 09/13] cache fix --- .docfx/nginx.conf | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.docfx/nginx.conf b/.docfx/nginx.conf index c95d1b1..291f27a 100644 --- a/.docfx/nginx.conf +++ b/.docfx/nginx.conf @@ -1,3 +1,10 @@ +# Only successful asset responses and revalidation responses may be cached. +map $status $fingerprinted_asset_cache_control { + default "no-store"; + ~^2 "public, max-age=31536000, immutable"; + 304 "public, max-age=31536000, immutable"; +} + server { listen 8080; server_name _; @@ -11,7 +18,7 @@ server { # cached permanently because changed content is published under a different URL. location ~ "^/public/.+-[A-Z0-9]{8}(?:-[A-Z0-9]{8})?(?:\.min)?\.(?:(?:css|js)(?:\.map)?|woff2?)$" { try_files $uri =404; - add_header Cache-Control "public, max-age=31536000, immutable" always; + add_header Cache-Control $fingerprinted_asset_cache_control always; add_header Vary "Accept-Encoding" always; } From 15c9b2500c1983b9c675eafeaf4912a6405aa7d0 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 19:26:14 +0200 Subject: [PATCH 10/13] =?UTF-8?q?=F0=9F=90=9B=20fix=20utc=20minimum=20drif?= =?UTF-8?q?ting=20with=20local=20time=20zone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CoordinatedUniversalTime.MinValue used DateTime.MinValue.ToUniversalTime(), which treats the Unspecified kind as local time and shifted the boundary on hosts west of UTC. It now marks the value as UTC directly, and a regression test pins it to zero ticks. --- .../CoordinatedUniversalTime.cs | 4 ++-- .../CoordinatedUniversalTimeMinValueTest.cs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 test/Codebelt.SharedKernel.Tests/CoordinatedUniversalTimeMinValueTest.cs diff --git a/src/Codebelt.SharedKernel/CoordinatedUniversalTime.cs b/src/Codebelt.SharedKernel/CoordinatedUniversalTime.cs index 63bd58b..f87c99a 100644 --- a/src/Codebelt.SharedKernel/CoordinatedUniversalTime.cs +++ b/src/Codebelt.SharedKernel/CoordinatedUniversalTime.cs @@ -17,9 +17,9 @@ public record CoordinatedUniversalTime : ComparableValueObject public static readonly CoordinatedUniversalTime MaxValue = DateTime.ParseExact("9999-12-31T22:59:59.9999999Z", "O", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); // Cannot use DateTime.MaxValue.ToUniversalTime() as it becomes 9999-12-31T23:59:59.9999999Z on Ubuntu 22.04 (but weirdly enough works on WSL with same distro). /// - /// Denotes the smallest possible value of . + /// Denotes the smallest possible value of , at midnight on January 1, 0001 UTC, regardless of the local time zone. /// - public static readonly CoordinatedUniversalTime MinValue = new(DateTime.MinValue.ToUniversalTime()); + public static readonly CoordinatedUniversalTime MinValue = new(DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc)); /// /// Performs an implicit conversion from to . diff --git a/test/Codebelt.SharedKernel.Tests/CoordinatedUniversalTimeMinValueTest.cs b/test/Codebelt.SharedKernel.Tests/CoordinatedUniversalTimeMinValueTest.cs new file mode 100644 index 0000000..69441dc --- /dev/null +++ b/test/Codebelt.SharedKernel.Tests/CoordinatedUniversalTimeMinValueTest.cs @@ -0,0 +1,22 @@ +using System; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Codebelt.SharedKernel; + +public class CoordinatedUniversalTimeMinValueTest : Test +{ + public CoordinatedUniversalTimeMinValueTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void MinValue_ShouldBeEarliestUtcInstant() + { + var sut = CoordinatedUniversalTime.MinValue; + + Assert.Equal(DateTime.MinValue.Ticks, sut.Value.Ticks); + Assert.Equal(DateTimeKind.Utc, sut.Value.Kind); + Assert.Equal("0001-01-01T00:00:00.0000000Z", sut.ToString()); + } +} From 3eed37decd826d44dcfcaf4190f4e4eef6d148a5 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 19:26:16 +0200 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=93=9D=20document=20utc=20minimum?= =?UTF-8?q?=20as=20fixed=20utc=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The namespace and CoordinatedUniversalTime API pages now state that MinValue is 0001-01-01T00:00:00.0000000Z on every host, independent of the local time zone. --- .docfx/api/namespaces/Codebelt.SharedKernel.md | 2 ++ .../api/types/Codebelt.SharedKernel.CoordinatedUniversalTime.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.docfx/api/namespaces/Codebelt.SharedKernel.md b/.docfx/api/namespaces/Codebelt.SharedKernel.md index b0af14f..4485f00 100644 --- a/.docfx/api/namespaces/Codebelt.SharedKernel.md +++ b/.docfx/api/namespaces/Codebelt.SharedKernel.md @@ -6,4 +6,6 @@ When domain code needs to talk about moments in time, traced requests, or immuta If you are issuing a token and need to express how long it remains valid, reach for `TimeToLive` and pass it to your issuer. If two systems must agree on what "now" means within a bounded margin, reach for `ClockSkew`. If you need an absolute timestamp, use `CoordinatedUniversalTime`. For traced request identifiers and opaque credentials, start with `CorrelationId` and `Secret`; both extend `Token` and inherit its minimum length, maximum length, and character-distribution checks. +`CoordinatedUniversalTime.MinValue` provides the fixed UTC lower boundary (`0001-01-01T00:00:00.0000000Z`) for validity ranges, independent of the host's local time zone. `AccessKeyOptions.ValidFrom` uses this boundary by default. + [!INCLUDE [](../../includes/availability-modern.md)] diff --git a/.docfx/api/types/Codebelt.SharedKernel.CoordinatedUniversalTime.md b/.docfx/api/types/Codebelt.SharedKernel.CoordinatedUniversalTime.md index 1e2770b..80b9841 100644 --- a/.docfx/api/types/Codebelt.SharedKernel.CoordinatedUniversalTime.md +++ b/.docfx/api/types/Codebelt.SharedKernel.CoordinatedUniversalTime.md @@ -3,6 +3,8 @@ uid: Codebelt.SharedKernel.CoordinatedUniversalTime example: - *content --- +`MinValue` is `0001-01-01T00:00:00.0000000Z` on every host. Its ticks are zero and its `DateTime.Kind` is `Utc`; the local time zone does not shift this boundary. + The following example captures the current UTC moment as a `CoordinatedUniversalTime` and converts it back to a `DateTime` for downstream consumers, and parses a wire-format ISO 8601 string the same way. Use `CoordinatedUniversalTime.Now` for "now" snapshots and `FromString` when an external system hands you a UTC timestamp that still needs validation. ```csharp From e6d0ad6136551a43ee4f8c5076cc2ede8256111a Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Sun, 18 Oct 2026 19:26:18 +0200 Subject: [PATCH 12/13] =?UTF-8?q?=F0=9F=92=AC=20update=20changelog=20for?= =?UTF-8?q?=20utc=20minimum=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 0.5.13 entry now records the time-zone-independent UTC minimum as a fix, with the investigation evidence in a collapsed section. --- CHANGELOG.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18676ea..d56ad40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,37 @@ For more details, please refer to `PackageReleaseNotes.txt` on a per assembly ba ## [0.5.13] - 2026-10-18 -This is a patch release focused on dependency maintenance, test diagnostics, documentation hosting, and repository guidance. +This is a patch release focused on dependency maintenance, test diagnostics, documentation hosting, repository guidance, and a time-zone-independent UTC minimum. + +> [!IMPORTANT] +> A longstanding time-zone dependency in `CoordinatedUniversalTime.MinValue` surfaced in Windows ARM64 CI after GitHub's [September 21–30, 2026 migration of `windows-11-arm` to the Visual Studio 2026 image (actions/runner-images#14602)](https://github.com/actions/runner-images/issues/14602). The image change is confirmed; a change to the runner's local time zone is a suspected trigger, not a confirmed upstream diagnosis. The fix makes the UTC minimum independent of the host's time zone. + +
+UTC minimum: investigation evidence and reproduction + +The original initializer, `DateTime.MinValue.ToUniversalTime()`, has been present since the [April 2, 2024 initial commit](https://github.com/codebeltnet/shared-kernel/blob/ab0ea1baf3b2b4fc4205501a52a5e137d39668b6/src/Codebelt.SharedKernel/CoordinatedUniversalTime.cs#L22). `DateTime.MinValue.Kind` is `Unspecified`, which [`ToUniversalTime()` treats as local time](https://learn.microsoft.com/en-us/dotnet/api/system.datetime.touniversaltime?view=net-10.0#remarks). In UTC−08:00 it becomes `0001-01-01T08:00:00.0000000Z`; in UTC it stays midnight. Positive local offsets can also produce midnight because conversion below `DateTime.MinValue` is clamped, masking the dependency. + +| Evidence | Observation | +| --- | --- | +| [September 14 passing Windows ARM64 Release job](https://github.com/codebeltnet/shared-kernel/actions/runs/34897095521/job/104154359999) | Image `windows-11-arm64`, version `20260906.161.1`; tests passed. | +| [October 18 failing Windows ARM64 Release job](https://github.com/codebeltnet/shared-kernel/actions/runs/39146717929/job/117732157882) | Image `windows-11-vs2026-arm64`, version `20261013.183.4`; `AccessKeyTest.Marshalling_ShouldRepresentCorrectly` expected midnight but received `08:00Z`. | +| [Unmodified September 14 `build-Release-X64` artifact](https://github.com/codebeltnet/shared-kernel/actions/runs/34897095521/artifacts/10368867368), reproduced locally on October 18 | The same archived .NET 10 test DLL passed in UTC and failed in UTC−08:00 with the identical `08:00Z` mismatch, using .NET 10.0.12 on Linux x64. The new package versions are not required to reproduce the failure. | + +To repeat the archived-artifact comparison, use Bash on Linux or WSL with .NET 10 and an authenticated GitHub CLI. Download the artifact while it remains available under GitHub's retention policy, then run the same DLL twice without rebuilding: + +```bash +baseline="$(mktemp -d)" +gh run download 34897095521 --repo codebeltnet/shared-kernel --name build-Release-X64 --dir "$baseline" +module="$baseline/test/Codebelt.SharedKernel.Tests/bin/Release/net10.0/Codebelt.SharedKernel.Tests.dll" +TZ=Etc/GMT+8 dotnet "$module" --filter-method '*AccessKeyTest.Marshalling_ShouldRepresentCorrectly' +TZ=UTC dotnet "$module" --filter-method '*AccessKeyTest.Marshalling_ShouldRepresentCorrectly' +``` + +`Etc/GMT+8` denotes UTC−08:00. The first invocation failed with `validFrom` at `08:00Z`; the second passed. Both used the archived assembly and its archived dependencies. These results establish the existing time-zone dependency, but do not establish which runner setting or runtime change first exposed it in CI. No matching upstream time-zone bug report was found during the October 18 investigation; #14602 documents the image migration only. + +The corrected initializer uses `DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc)`, preserving zero ticks without local-time conversion. The new minimum-value regression test failed before the fix in UTC−08:00. After the fix, all 229 tests per target framework passed on .NET 9 and .NET 10 under UTC−08:00, UTC, and UTC+08:00 in Linux x64, and under the local Copenhagen time zone in Windows x64. A new GitHub Windows ARM64 run is still required to verify the fix on the affected hosted image. + +
### Added @@ -27,6 +57,10 @@ This is a patch release focused on dependency maintenance, test diagnostics, doc - Direct test dependency on `Microsoft.Testing.Extensions.CodeCoverage`, - Deprecated `.github/copilot-instructions.md`; its guidance is maintained in `AGENTS.md`. +### Fixed + +- `CoordinatedUniversalTime.MinValue` now consistently represents `0001-01-01T00:00:00.0000000Z`, preventing local time-zone offsets from changing the default `AccessKey.ValidFrom` value. + ## [0.5.12] - 2026-09-14 This is a patch release focused on test infrastructure modernization, build system improvements, and dependency upgrades to their latest compatible versions across all supported target frameworks. From 2da46c5fdae377c35a5613992737e7547561a71f Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 18 Oct 2026 19:32:40 +0200 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=93=A6=20add=20utc=20minimum=20fix?= =?UTF-8?q?=20to=20nuget=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt b/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt index f247856..930b6b5 100644 --- a/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt @@ -4,6 +4,9 @@ Availability: .NET 10 and .NET 9 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +# Bug Fixes +- FIXED CoordinatedUniversalTime.MinValue to consistently represent 0001-01-01T00:00:00.0000000Z regardless of the host time zone, preventing an offset in the default AccessKey.ValidFrom value. This longstanding defect surfaced after GitHub's Windows ARM64 runner image migration (https://github.com/actions/runner-images/issues/14602); the precise runner trigger remains unconfirmed. See CHANGELOG.md for CI evidence and reproduction steps + Version: 0.5.12 Availability: .NET 10 and .NET 9