Skip to content

Latest commit

 

History

History
670 lines (508 loc) · 38 KB

AGENTS.md

File metadata and controls

670 lines (508 loc) · 38 KB

AGENTS.md

This file guides agentic coding tools working in this repo. Follow the repo rules first; do not invent commands or conventions. Keep all shared repository instructions in AGENTS.md.

Repository Layout

  • Solution: Cuemon.slnx in repo root.
  • src/ — NuGet packages (shipped to nuget.org).
  • test/ — xUnit v3 unit and functional tests.
  • tuning/ — BenchmarkDotNet benchmarks.
  • tooling/ — internal CLI tools.
  • .nuget/<PackageName>/ — per-package README.md and PackageReleaseNotes.txt.

Toolchain

  • .NET SDK with LangVersion=latest.
  • Source TFMs: net10.0;net9.0;netstandard2.0.
  • Test TFMs: net10.0;net9.0 on Linux; adds net48 on Windows.
  • Benchmark TFMs: net10.0;net9.0.
  • Central package management via Directory.Packages.props (ManagePackageVersionsCentrally=true).
  • CI runs on Linux (ubuntu-26.04) and Windows (windows-2025), both X64 and ARM64.
  • TFM compatibility is mandatory: proposals and code changes must work for all source TFMs. Do not assume net9.0/net10.0 APIs exist in netstandard2.0; use conditional compilation (#if NET9_0_OR_GREATER) or compatible fallbacks where needed.

Build Commands

dotnet build -c Release                                           # entire solution
dotnet build src/Cuemon.Core/Cuemon.Core.csproj -c Release        # single project
dotnet pack -c Release                                            # pack all NuGet packages

Lint / Analyzers

  • No separate lint step; code style is enforced during build (EnforceCodeStyleInBuild=true for source projects).
  • Analyzers are disabled for test and benchmark projects (RunAnalyzers=false, AnalysisLevel=none).
  • Run dotnet build -c Release on source projects to surface style violations.

Test Commands

Development Workflow (Fast Feedback Loop)

Recommended for rapid iteration: Run tests only on affected projects instead of the full test suite (40+ projects). This provides fast feedback during development.

# Single test class (fastest)
dotnet test test/Cuemon.Core.Tests/Cuemon.Core.Tests.csproj -c Release

# Single test method (most focused)
dotnet test test/Cuemon.Core.Tests/Cuemon.Core.Tests.csproj -c Release \
  --filter "FullyQualifiedName~DateSpanTest.Parse_ShouldGetOneMonthOfDifference_UsingIso8601String"

# Multiple affected projects (example: if you changed Cuemon.Core, test Core + projects that depend on it)
dotnet test test/Cuemon.Core.Tests/Cuemon.Core.Tests.csproj \
             test/Cuemon.Extensions.Core.Tests/Cuemon.Extensions.Core.Tests.csproj -c Release

Strategy: Identify which test projects depend on your modified production code (via project references in .csproj files or namespace imports). Run only those test projects to catch regressions quickly. Full suite validation runs in CI.

Full Test Suite (Comprehensive Validation)

Before committing or when validating across the entire codebase, run all tests. Note: Some projects require external dependencies.

# Start SQL Server and dependencies (required for full test suite)
docker compose up -d

# Run all tests
dotnet test -c Release test/

# Clean up when done
docker compose down

Integration Tests (SQL Server)

  • Project: test/Cuemon.Data.SqlClient.Tests/Cuemon.Data.SqlClient.Tests.csproj.
  • Requires env var CONNECTIONSTRINGS__ADVENTUREWORKS.
  • Before running full test suite: Start the SQL Server container via docker compose up -d (defined in root docker-compose.yml).

Benchmarks

  • Live under tuning/; run with tooling/bdn-runner.
  • Not unit tests; do not include in test runs.

Code Style and Conventions

General Principles

  • Follow Framework Design Guidelines and Microsoft Engineering Guidelines.
  • Adhere to SOLID, DRY, separation of concerns.
  • Apply the boy scout rule; do not duplicate code.

Formatting

  • 4 spaces for .cs / .vb; 2 spaces for .xml (.editorconfig).
  • Keep existing style in files; many modern analyzers are explicitly disabled.

Namespace Style

  • Prefer file-scoped namespaces (namespace Cuemon.Foo;) for new files.
  • The current majority of the codebase uses block-scoped namespaces — do not convert existing files unless explicitly asked.
  • When editing an existing file, follow whichever style that file already uses.
  • Never use top-level statements. Always use explicit class declarations with a proper namespace.

Disabled Analyzers (key rules — do NOT introduce these patterns)

Rule What it forces Why disabled
IDE0066 switch expressions style consistency
IDE0063 using declarations style consistency
IDE0290 primary constructors style consistency
IDE0022 expression-bodied methods style consistency
IDE0300/0301/0028/0305 collection expressions netstandard2.0 compat
CA1846/1847/1865-1867 Span/char overloads netstandard2.0 compat
IDE0330 System.Threading.Lock requires net9.0+
Performance category various netstandard2.0 compat

Imports

  • Keep using directives explicit and minimal.
  • Follow existing ordering; do not auto-reorder.

Types and var

  • Do not blindly enforce var; use explicit types when it improves clarity.
  • IDE0008 (use explicit type) is disabled — either form is acceptable.

Naming

  • Public API naming follows .NET Framework Design Guidelines.
  • Test classes end with Test; benchmark classes end with Benchmark.
  • Namespaces for tests and benchmarks must match the production namespace exactly (no .Tests / .Benchmarks suffix). Override <RootNamespace> in .csproj.

Error Handling

  • Use guard clauses and Validator.ThrowIfNull patterns.
  • Prefer deterministic, testable error paths; never swallow exceptions.

Extension Methods

  • Extension methods belong only in Cuemon.Extensions.* projects.
  • Non-extension assemblies may expose similar APIs only behind the IDecorator interface.

Writing Tests

These instructions apply when creating or editing test code and test project files (.cs and .csproj). Follow them for hand-written tests and generated scaffolding.

Base Class and Test Output

  • Test framework: xUnit v3 (xunit.v3 package).
  • Always inherit from Test in Codebelt.Extensions.Xunit for every unit test class. This ensures consistent setup, teardown, and output handling.
  • Do NOT add using Xunit.Abstractions or depend on Xunit.Abstractions. xUnit v3 no longer exposes that namespace; importing it causes compilation errors. Use using Codebelt.Extensions.Xunit; and using Xunit; as shown in the example below.
  • Constructor signature: public FooTest(ITestOutputHelper output) : base(output) { }.
  • For test output, rely on the inherited TestOutput property and use TestOutput.WriteLine(...). Pass the output helper to the base constructor.

Attributes, Naming, and Assertions

  • Use [Fact] for unit tests, [Theory] with [InlineData] or other data sources for parameterized tests.
  • Test class names must end with Test, e.g., DateSpanTest.
  • Test method names must describe the expected behavior and relevant condition, e.g., ShouldReturnTrue_WhenConditionIsMet.
  • Use xUnit Assert.* methods for all assertions. Prefer explicit, expressive assertions such as Assert.Equal, Assert.NotNull, and Assert.Contains.
  • Keep tests focused, isolated, deterministic, and repeatable. Unit tests must not rely on external systems beyond xUnit and Codebelt.Extensions.Xunit (or its derived testing support). Integration dependencies are covered under Test Commands.

File, Assembly, and Namespace Organization

  • Place test files in the appropriate test project and folder structure. Align folders with production code so SUT/test pairs are easy to locate.
  • A test namespace MUST match the exact namespace of the System Under Test (SUT). Do NOT append .Tests, .Benchmarks, or any similar suffix. Only the assembly/project name identifies the testing role.
  • Unit tests for Cuemon.Foo live in Cuemon.Foo.Tests; functional tests live in Cuemon.Foo.FunctionalTests.
  • For a production Boo class in namespace Cuemon.Foo, use BooTest in namespace Cuemon.Foo within Cuemon.Foo.Tests.
  • When generating test scaffolding, resolve the SUT namespace from the source file or project/assembly metadata and use that exact namespace in the file header.
  • Override the test project's root namespace to match production. For Cuemon.Foo, use:
<PropertyGroup>
  <RootNamespace>Cuemon.Foo</RootNamespace>
</PropertyGroup>

For a SUT in namespace Cuemon.Foo.Bar, the following illustrates the distinction:

Role Assembly/project Namespace Class
Production Cuemon.Foo Cuemon.Foo.Bar Zoo
Unit test Cuemon.Foo.Tests Cuemon.Foo.Bar ZooTest
Functional test Cuemon.Foo.FunctionalTests Cuemon.Foo.Bar ZooTest

Namespaces such as Cuemon.Foo.Bar.Tests and Cuemon.Foo.Bar.Benchmarks are incorrect. Exact namespace matching supports consistent type discovery and XML documentation links and reduces confusion when reading tests.

Test Doubles

  • Prefer dummies, fakes, stubs, and spies when the design allows them.
  • Use mocks with Moq only under special circumstances.
  • Before overriding a method, verify that it is virtual or abstract. This requirement also applies when configuring mocks.
  • Never mock IMarshaller. Always use a new instance of JsonMarshaller.

Testing Internal Implementations

Do NOT use InternalsVisibleTo to expose internal types or members to test projects. Internal classes and methods must be validated by exercising the public APIs that consume them: public facades, public extension methods, or other public entry points.

The preferred pattern is Public Facade Testing, also called Public API Proxy Testing. Tests must assert observable public behavior rather than target internal implementation details directly.

Example mapping:

Element Example
Internal helper DelimitedString (internal static class)
Public entry point TestOutputHelperExtensions.WriteLines() (public extension method)
Test strategy Test WriteLines() and verify its public behavior; this implicitly exercises DelimitedString.Create().

Apply this pattern when:

  • 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.

This avoids exposing internals to test assemblies, reflects real usage, maintains encapsulation and a clean public API, and keeps tests resilient to internal refactoring while public behavior is preserved.

Code Coverage

Do NOT use the ExcludeFromCodeCoverage attribute on any code. This prohibition covers test classes, test methods, production code, configuration code, and every other code path.

Exclusions hide coverage gaps and create false confidence in test completeness. Every executable line should be covered by tests or be genuinely unreachable dead code that must be removed. If a path cannot or should not be tested, refactor to eliminate the path instead of hiding it from metrics.

Use these alternatives:

Situation Required approach
Untestable code path Refactor to separate concerns and eliminate the untestable path.
External dependency Use fakes, stubs, or spies instead of excluding coverage.
Configuration-only code Move it to configuration files or extract testable methods.
Generated or third-party code Keep it outside the primary codebase; use NuGet packages or dedicated vendor folders where necessary.

Complete Test Example

This example uses fixed inputs, explicit component and total-value assertions, and output through the Test base class.

using System;
using Codebelt.Extensions.Xunit;
using Xunit;

namespace Cuemon;

/// <summary>
/// Tests for the <see cref="DateSpan"/> class.
/// </summary>
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());

        TestOutput.WriteLine(span.ToString());
    }
}

For further examples, see DateSpanTest.cs, DisposableTest.cs, and HashFactoryTest.cs.

Writing Benchmarks

These instructions apply to tuning/** and benchmark files matching **/*Benchmark*.cs. Use BenchmarkDotNet to keep benchmarks consistent, readable, and comparable.

Naming and Placement

  • Place micro- and component-benchmarks in the appropriate project and folders under tuning/, in a *.Benchmarks project. Keep these projects isolated from package builds and production artifacts.
  • Benchmarks for Cuemon.Bar belong in Cuemon.Bar.Benchmarks. A benchmark for Zoo is named ZooBenchmark; class names must end with Benchmark.
  • Benchmark namespaces MUST match the production namespace exactly. Do NOT append .Benchmarks or a similar suffix. For Cuemon.Security.Cryptography, use namespace Cuemon.Security.Cryptography and a class such as Sha512256Benchmark in the Cuemon.Security.Cryptography.Benchmarks project.
  • Override <RootNamespace> in the benchmark .csproj to match production, e.g., <RootNamespace>Cuemon.Bar</RootNamespace>.

Attributes and Configuration

  • Use [Benchmark] on methods representing measured operations.
  • Use [MemoryDiagnoser] to capture allocations and [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] to group related benchmarks.
  • Use [Params] for input sizes or variations and [GlobalSetup] for one-time initialization that is outside measured work.
  • Mark one comparison method Baseline = true; provide descriptive Description values to make reports readable.
  • Keep configuration minimal and explicit; prefer in-class attributes over large shared configurations unless widely reused.

Structure and Measured Work

  • Measure a single logical operation per benchmark method. Keep expensive preparation outside measured methods, using [GlobalSetup], [IterationSetup], or cached fields as appropriate.
  • Use [Params] for input variations and small, medium, and large input sizes to verify performance trends. Use small, deterministic data sets.
  • Avoid external systems (network, disk, databases). If necessary, clearly mark those benchmarks and exclude them from CI benchmark runs by default.
  • Use descriptive method names that identify the scenario, e.g., Parse_Short or ComputeHash_Large. When comparing implementations, use similar method names so reports are easy to read.
  • Capture meaningful measurements: time, allocations, and custom counters where needed.

Complete Benchmark Example

This example compares creating the same DateSpan directly and parsing it from ISO 8601 strings. It demonstrates parameterized inputs, deterministic setup, memory measurements, a baseline, and descriptions using real Cuemon APIs.

using System;
using System.Globalization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;

namespace Cuemon;

[MemoryDiagnoser]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
public class DateSpanBenchmark
{
    [Params(1, 31, 3650)]
    public int Days { get; set; }

    private DateTime _start;
    private DateTime _end;
    private string _startString;
    private string _endString;

    [GlobalSetup]
    public void Setup()
    {
        _start = new DateTime(2021, 3, 5);
        _end = _start.AddDays(Days);
        _startString = _start.ToString("O", CultureInfo.InvariantCulture);
        _endString = _end.ToString("O", CultureInfo.InvariantCulture);
    }

    [Benchmark(Baseline = true, Description = "DateSpan - constructor")]
    public DateSpan Create_Constructor()
    {
        return new DateSpan(_start, _end, CultureInfo.InvariantCulture.Calendar);
    }

    [Benchmark(Description = "DateSpan - ISO 8601 parsing")]
    public DateSpan Create_Parse()
    {
        return DateSpan.Parse(_startString, _endString, CultureInfo.InvariantCulture);
    }
}

Reporting, CI, and Shared Helpers

  • Benchmarks primarily run locally for tuning. Prefer targeted runs or harnesses for CI; avoid heavy BenchmarkDotNet workloads there by default.
  • Keep benchmarks readable and documented; comment on non-obvious choices. When a benchmark exposes a regression or optimization, add a short note in its file referencing the relevant issue or PR.
  • Keep shared benchmark helpers as small utility classes within tuning projects instead of changing production code for benchmark support.

For concrete examples, see DateSpanBenchmark.cs and Sha512256Benchmark.cs.

XML Documentation

These instructions apply to C# files (**/*.cs).

  • All public and protected types and members should have XML doc comments, including classes, constructors, methods, and properties.
  • Follow existing wording and style in the codebase.
  • Use <summary> to describe types and members, <typeparam> for generic parameters, <param> for method and constructor parameters, <returns> for return values, and <value> for properties.
  • Use <see cref="..."/> and <seealso cref="..."/> for type/member references and <paramref name="..."/> for parameter references. Generic references use braces, e.g., Hash{TOptions}.
  • Follow constructor wording such as "Initializes a new instance of the ... class" and property wording such as "Gets or sets ...".
  • Document initial configuration values in constructor <remarks> using a <list type="table"> with property and initial-value columns, as illustrated below.

Generic Types, Protected Members, and Overloads

The following example from Hash.cs shows documentation for a generic base class, its type parameter and related interfaces, a protected constructor, a read-only property, a sealed override, and public and protected abstract methods. Representative overloads illustrate scalar, string, array, sequence, and stream inputs, optional configuration, and return-value documentation. Apply the same documentation pattern to each overload in the full API.

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;
/// <summary>
/// Represents the base class from which all implementations of hash algorithms and checksums should derive.
/// </summary>
/// <typeparam name="TOptions">The type of the configured options.</typeparam>
/// <seealso cref="ConvertibleOptions"/>
/// <seealso cref="IConfigurable{TOptions}" />
/// <seealso cref="IHash" />
public abstract class Hash<TOptions> : Hash, IConfigurable<TOptions> where TOptions : ConvertibleOptions, new()
{
    /// <summary>
    /// Initializes a new instance of the <see cref="Hash{TOptions}"/> class.
    /// </summary>
    /// <param name="setup">The <see cref="ConvertibleOptions" /> which may be configured.</param>
    protected Hash(Action<TOptions> setup)
    {
        Options = Patterns.Configure(setup);
    }

    /// <summary>
    /// Gets the configured options of this instance.
    /// </summary>
    /// <value>The configured options of this instance.</value>
    public TOptions Options { get; }


    /// <summary>
    /// The endian-initializer of this instance.
    /// </summary>
    /// <param name="options">An instance of the configured options.</param>
    protected sealed override void EndianInitializer(EndianOptions options)
    {
        options.ByteOrder = Options.ByteOrder;
    }
}

/// <summary>
/// Represents the base class that defines the public facing structure to expose.
/// </summary>
/// <seealso cref="IHash" />
public abstract class Hash : IHash
{
    /// <summary>
    /// Initializes a new instance of the <see cref="Hash"/> class.
    /// </summary>
    protected Hash()
    {
    }

    /// <summary>
    /// Computes the hash value for the specified <see cref="bool"/>.
    /// </summary>
    /// <param name="input">The <see cref="bool"/> to compute the hash code for.</param>
    /// <returns>A <see cref="HashResult"/> containing the computed hash code of the specified <paramref name="input"/>.</returns>
    public virtual HashResult ComputeHash(bool input)
    {
        return ComputeHash(Convertible.GetBytes(input, EndianInitializer));
    }

    /// <summary>
    /// Computes the hash value for the specified <see cref="string"/>.
    /// </summary>
    /// <param name="input">The <see cref="string"/> to compute the hash code for.</param>
    /// <param name="setup">The <see cref="EncodingOptions"/> which may be configured.</param>
    /// <returns>A <see cref="HashResult"/> containing the computed hash code of the specified <paramref name="input"/>.</returns>
    public virtual HashResult ComputeHash(string input, Action<EncodingOptions> setup = null)
    {
        return ComputeHash(Convertible.GetBytes(input, setup));
    }

    /// <summary>
    /// Computes the hash value for the specified <see cref="IConvertible"/> array.
    /// </summary>
    /// <param name="input">The <see cref="IConvertible"/> array to compute the hash code for.</param>
    /// <returns>A <see cref="HashResult"/> containing the computed hash code of the specified <paramref name="input"/>.</returns>
    public virtual HashResult ComputeHash(params IConvertible[] input)
    {
        return ComputeHash(Arguments.ToEnumerableOf(input));
    }

    /// <summary>
    /// Computes the hash value for the specified sequence of <see cref="IConvertible"/>.
    /// </summary>
    /// <param name="input">The sequence of <see cref="IConvertible"/> to compute the hash code for.</param>
    /// <returns>A <see cref="HashResult"/> containing the computed hash code of the specified <paramref name="input"/>.</returns>
    public virtual HashResult ComputeHash(IEnumerable<IConvertible> input)
    {
        return ComputeHash(Convertible.GetBytes(input));
    }

    /// <summary>
    /// Computes the hash value for the specified <see cref="byte"/> array.
    /// </summary>
    /// <param name="input">The <see cref="byte"/> array to compute the hash code for.</param>
    /// <returns>A <see cref="HashResult"/> containing the computed hash code of the specified <paramref name="input"/>.</returns>
    public abstract HashResult ComputeHash(byte[] input);

    /// <summary>
    /// Computes the hash value for the specified <see cref="Stream"/>.
    /// </summary>
    /// <param name="input">The <see cref="Stream"/> to compute the hash code for.</param>
    /// <returns>A <see cref="HashResult"/> containing the computed hash code of the specified <paramref name="input"/>.</returns>
    public virtual HashResult ComputeHash(Stream input)
    {
        return ComputeHash(Patterns.SafeInvoke(() => new MemoryStream(), destination =>
        {
            Decorator.Enclose(input).CopyStream(destination);
            return destination;
        }).ToArray());
    }

    /// <summary>
    /// Defines the initializer that <see cref="Hash{TOptions}"/> must implement.
    /// </summary>
    /// <param name="options">An instance of the configured options.</param>
    protected abstract void EndianInitializer(EndianOptions options);
}

Configuration Defaults Example

namespace Cuemon.Security;

/// <summary>
/// Configuration options for <see cref="FowlerNollVoHash"/>.
/// </summary>
public class FowlerNollVoOptions : ConvertibleOptions
{
    /// <summary>
    /// Initializes a new instance of the <see cref="FowlerNollVoOptions"/> class.
    /// </summary>
    /// <remarks>
    /// The following table shows the initial property values for an instance of <see cref="FowlerNollVoOptions"/>.
    /// <list type="table">
    ///     <listheader>
    ///         <term>Property</term>
    ///         <description>Initial Value</description>
    ///     </listheader>
    ///     <item>
    ///         <term><see cref="EndianOptions.ByteOrder"/></term>
    ///         <description><see cref="Endianness.BigEndian"/></description>
    ///     </item>
    ///     <item>
    ///         <term><see cref="Algorithm"/></term>
    ///         <description><see cref="FowlerNollVoAlgorithm.Fnv1a"/></description>
    ///     </item>
    /// </list>
    /// </remarks>
    public FowlerNollVoOptions()
    {
        Algorithm = FowlerNollVoAlgorithm.Fnv1a;
        ByteOrder = Endianness.BigEndian;
    }

    /// <summary>
    /// Gets or sets the algorithm of the Fowler-Noll-Vo hash function.
    /// </summary>
    /// <value>The algorithm of the Fowler-Noll-Vo hash function.</value>
    public FowlerNollVoAlgorithm Algorithm { get; set; }
}

See Hash.cs for generic type, protected constructor, method, overload, and return-value documentation, and FowlerNollVoOptions.cs for the complete configuration example.

Release Notes

  • Per-package notes in .nuget/<PackageName>/PackageReleaseNotes.txt.
  • Keep updated for public API changes.

Commit Style (Gitmoji)

This repo uses gitmoji commit messages — do not use Conventional Commits (feat:, fix:, etc.).

Format: <emoji> <subject>

Always use the actual Unicode emoji character, not the GitHub shortcode (e.g., use ✨ not :sparkles:).

Example: ✨ Add DateSpan.TryParse overload

Common Gitmojis

Emoji Use for
✨ New feature
🐛 Bug fix
♻️ Refactoring
✅ Adding / updating unit test / functional test
📝 Documentation
⚡ Performance improvement
🎨 Code style / formatting
🔥 Removing code or files
🚧 Work in progress
📦 Package / dependency update
🔧 Configuration / tooling
🚚 Moving / renaming files
💥 Breaking change
🩹 Non-critical fix

Rules

  1. One emoji per commit — each commit has exactly one primary gitmoji.
  2. Be specific — choose the most appropriate emoji, not a generic one.
  3. Consistent scope — use consistent scope names across commits.
  4. Clear messages — the subject line should be understandable without a body.
  5. Atomic commits — each commit should be independently buildable and testable.

Git Operations Safeguards

Agents must never automatically commit code changes or push to remote repositories. Both actions require explicit user approval:

  • Commits: Always request confirmation from the user before staging and committing code. Present a clear summary of the changes and wait for approval before executing the commit.
  • Remote Operations: Do not push, pull, fetch, or interact with origin or any remote repository without explicit user instruction. These operations modify repository history and can cause data loss if performed unexpectedly.

Rationale: Automatic commits can clutter history with incomplete work, temporary debugging code, or unintended changes. Unexpected remote operations risk overwriting or losing commits on shared branches. Always require explicit user approval before performing these actions.

Agent Workflow

  1. Identify the correct project area (src/, test/, tuning/, tooling/).
  2. Follow namespace and naming rules before writing any code.
  3. Before potentially refactoring any code, verify the code in question is well tested; if coverage is missing, add or update tests first to reduce regression risk.
  4. Build the affected source project to check for style violations.
  5. Run targeted tests when changing logic.
  6. Keep changes minimal and consistent with existing local style.

DocFX Documentation Maintenance

When changing public .NET APIs, keep the DocFX documentation current in the same change set.

Documentation updates must cover public API only. Do not document private or internal types or members. Do not create namespace overview pages for namespaces that contain no public API.

Public non-abstraction types — including enums, structs, records, plain classes, and static extension containers — are valid documentation targets. Generic public types and generic extension methods are valid documentation targets too. Do not exclude a type solely because it is generic or because reflection reports it as abstract and sealed (that is the IL pattern for a static class).

For public non-abstraction types, include at least one realistic, copy/paste-ready usage example on the generated type page/overwrite section for that type UID. For example, a public Class1 requires an example on the Class1 API page, not only on the namespace page. Prefer deriving examples from existing unit, functional, or integration tests, but convert test code into real-life consumer-oriented usage.

Missing type examples must be added through per-type DocFX overwrite files under .docfx/api/types/{TypeUid}.md in Codebelt repositories. Namespace overview text and Extension Members tables are not substitutes for type-page examples.

Public extension methods must have examples too. Listing an extension method in an Extension Members table is required, but it is not enough.

All added or changed code samples must be deterministic and verified to compile. Do not add pseudo-code, ellipses, hidden test helpers, or examples that rely on unverified behavior.

Compilation is necessary but not sufficient. Do not present runtime implementation names such as services.GetType().Name or host.GetType().FullName as the example outcome. Show application behavior, configured state, a resolved domain service, an HTTP response, or another result that explains why a caller uses the API. Application-entry-point examples must not declare an empty local Program type merely to compile; show a real entry point or clearly identify the referenced application project.

Every namespace containing public API must have a DocFX namespace overview page named after the namespace, such as X.Y.Z.md, under .docfx/api/namespaces/, using DocFX overwrite front matter with the namespace uid.

Namespace pages must identify key entry points from release notes, package documentation, public factories/builders, and strong functional tests, then help readers choose among adjacent workflows. When the package complements a well-known upstream API, compare concrete acquisition, customization, lifecycle, and sharing tradeoffs from current official guidance; do not claim drop-in replacement compatibility without evidence.

Namespaces exposing public extension methods must document those extension members at namespace level. The namespace page must include an Extension Members table listing the extended type, the extension marker, and the public extension methods. Extension members are rendered under the heading Extension Members.

Both namespace overwrite files and type overwrite files are required deliverables in the same run. Generating only namespace pages or only type pages is incomplete.

docfx.json must keep namespace and type overwrite files in separate subdirectories. build.overwrite must include both api/namespaces/**/*.md (for namespace pages) and api/types/**/*.md (for type pages). build.content must exclude both api/namespaces/** and api/types/** to prevent overwrite Markdown from being treated as conceptual content. Do not use api/**/*.md under build.overwrite or build.content.

Availability must be documented by referencing the appropriate include file when one exists, or by adding explicit availability text when no suitable include exists. Availability must reflect the actual target frameworks, conditional compilation, and project configuration.

For conditionally compiled APIs, choose the executable test framework from the asset that contains the API. Inspect the preprocessor condition, project TFMs, package lib/ assets, and resolved consumer asset before changing a sample. For APIs under NETSTANDARD2_0 or NETSTANDARD2_0_OR_GREATER, when modern lib/netX.0/ assets also exist, use net48 (or another supported .NET Framework target from net462 onward) so the consumer selects lib/netstandard2.0/. Never use netstandard* as an executable target, and never use a modern netX.0 target when it selects an asset where the API is absent. For other TFM guards, select a runnable consumer TFM that resolves to the containing asset and confirm that selection from restore or build evidence.

Preserve manual documentation edits. Prefer additive changes, but correct stale or contradictory information so documentation remains accurate.

Preserve working Markdown links, Related: references, and historical URL citations during prose rewrites. Remove or replace a URL only after directly verifying that the current destination returns HTTP 404. Timeouts, 403s, rate limits, DNS failures, and other lookup problems are not removal evidence.

The following restrictions on new working-tree files apply only to files created as part of the DocFX documentation-maintenance process, not to the repository in general. Interim scratch artifacts from this process do not belong in the repository working tree. Store assessment queues, project manifests, review reports, captured validator output, progress notes, and one-off helper scripts in temp or session storage instead. New working-tree files are only legitimate when they are the managed AGENTS.md block, the active docfx.json, the deterministic skip-compile-allowlist.json waiver file when one is truly required, or DocFX-authored namespace/type Markdown that maps to a real public namespace or type. Everything else is blocking cleanup work, not a documentation deliverable. The validator auto-detects generic-arity type families (such as MutableTuple1..MutableTupleN) and skips redundant sibling examples from the public API surface alone, so no family-skip manifest is ever written into the repository.

Skip markers are waivers, not fixes. A skip marker only suppresses compilation when it both existed before the current run and matches an entry in .docfx/skip-compile-allowlist.json. Each allowlist entry must include diagnosticCode, filePath, uid or symbol, reason, approval, and lifetime (temporary or permanent). Newly introduced or unallowlisted skip markers remain fail-level diagnostics and do not permit a completion claim.

Do not emit a final report, audit result, completion summary, or handoff while summary.canClaimCompletion is false, summary.remainingWorkItems is greater than zero, summary.remainingGates is non-empty, summary.fullVerificationRan is false, fail-level diagnostics remain, summary.newlyIntroducedSkipMarkers is non-zero, or summary.interimArtifacts is non-zero. Large queues, many changed files, repetitive next steps, long runtimes, context pressure, session length, task size, or a "stable queue" are not valid stop reasons; the next action must be another remediation batch, a validator rerun, a validator/tooling fix, or a true blocker with exact evidence.

Context pressure is not a completion condition. If the session feels constrained while work remains, continue with a smaller deterministic batch, regenerate deterministic queue state such as --assessment-queue, --project-manifest, or the active dry-run manifest/review pair, or report a true tooling failure with the exact command, exit code, and output. When naming a queue-state regeneration command, resolve it to a concrete temp/session path instead of leaving <temp-path> as a placeholder. Do not stop with phrases like "given context constraints", "best done in a follow-up", "remaining work requires authoring", "this is a massive task", or "I will provide a focused summary". A context-sized handoff while work remains is FAIL_CONTEXT_HANDOFF_WITH_REMAINING_WORK; the remediation is to continue with a smaller deterministic batch.

Before completing documentation work, run the relevant verification commands, normally:

dotnet build
dotnet test
dotnet run --file <resolved-skill-dir>/scripts/docfx.cs -- --repo-root . --build-api-model --validate-samples --verify-docfx-build

Codebelt repositories are normally strong-name signed with a .snk file in the repository root on the main author's codespace. Preserve and copy that root .snk file when building a temporary copy. If the repository or temp copy has no root .snk, run build and test verification with -p:SkipSignAssembly=true, for example dotnet build -p:SkipSignAssembly=true and dotnet test -p:SkipSignAssembly=true.

The final DocFX verification must run outside the working tree when possible. The --verify-docfx-build option copies the repository to a temp workspace, runs DocFX against the resolved docfx.json there, and removes the temp workspace afterward so generated API YAML, manifest files, and site output do not flood git status. Do not call the work complete until the final JSON reports summary.fullVerificationRan: true, summary.canClaimCompletion: true, summary.remainingWorkItems: 0, an empty summary.remainingGates, an empty summary.remainingDiagnosticsByCode, summary.newlyIntroducedSkipMarkers: 0, and summary.interimArtifacts: 0.

If a command cannot be run, report the exact limitation or failure instead of claiming the documentation was verified.