From 11b927267c794c89b880e16db5d887b7a65010c3 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 21 Jun 2026 21:40:36 -0700 Subject: [PATCH 1/7] Re-sync from ProjectTemplate and swap Todo VS Code add-on Reconverge the repo docs and CI config with upstream ProjectTemplate: add HISTORY.md and CODESTYLE.md, narrow copilot-instructions.md to the Copilot review runbook, restructure AGENTS.md (correct the versioning policy to bump only for functional changes, add documentation-style, verbatim-carry, and upstream-sync sections), mark the .editorconfig .NET-only boundary, and set retention-days on the build artifact upload. Convert the README version list to a Release Notes summary linking HISTORY.md, and record the Todo add-on swap (Todo Tree -> Better Todo Tree) as the v3.5 change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .editorconfig | 3 + .github/copilot-instructions.md | 441 ++---------------- .github/workflows/build-nugetlibrary-task.yml | 1 + AGENTS.md | 153 +++++- CODESTYLE.md | 315 +++++++++++++ HISTORY.md | 18 + README.md | 18 +- Utilities.code-workspace | 8 +- 8 files changed, 513 insertions(+), 444 deletions(-) create mode 100644 CODESTYLE.md create mode 100644 HISTORY.md diff --git a/.editorconfig b/.editorconfig index de52618..8e32cdd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -47,6 +47,9 @@ end_of_line = lf [*.{cmd,bat,ps1}] end_of_line = crlf +# --- .NET-only below: C# and ReSharper style. Everything above is the line-ending +# governance every derived repo carries; a non-.NET repo may drop from here down. --- + # C# files [*.cs] diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5bfa2b0..1ac3a75 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,427 +1,36 @@ -# GitHub Copilot Instructions for Utilities Project +# Copilot Instructions -## Project Overview +Repository conventions for GitHub Copilot (and any other AI agent reading this file). -This is a .NET utility library that provides generally useful C# classes and extensions. The project targets .NET 10 and includes AOT (Ahead-of-Time) compilation support for optimized runtime performance. +The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract this file's runbook implements. This file is intentionally narrow: commit/PR-title conventions (summarized inline so VS Code's commit-message and PR-title generators have them) plus the GitHub Copilot Review Runbook. -## Code Style and Standards +For language-specific style rules, see [`CODESTYLE.md`](../CODESTYLE.md) at the repo root. -### General Guidelines +Do not duplicate language-specific rules here. **Project-specific conventions and API/behavioral contracts also belong in [AGENTS.md](../AGENTS.md), not here** - this file is intentionally limited to the inline commit/PR-title summary and the GitHub Copilot Review Runbook. Non-Copilot agents (Claude Code, Codex, Cursor, ...) are not directed to this file and don't read it by default, so any rule a reviewer must honor has to live in `AGENTS.md` to be provider-independent. -- Follow C# coding conventions and .NET best practices -- Use meaningful variable and method names -- Keep methods focused and single-purpose -- **Add comprehensive XML documentation comments for ALL public APIs** (required) -- Maintain consistency with existing code style -- Follow the existing patterns in the codebase +## Commit Messages and Pull Request Titles -### Formatting Requirements +Summarized for VS Code's generators; the full rules, rationale, and examples are in [AGENTS.md "Pull Request Title and Commit Message Conventions"](../AGENTS.md#pull-request-title-and-commit-message-conventions). -**IMPORTANT:** This project uses **Husky.Net** pre-commit hooks that automatically enforce formatting: - -1. **CSharpier** is run first for code formatting -2. **dotnet format** is run second for style enforcement - -#### Formatting Workflow - -```bash -# Always format with CSharpier FIRST after editing code -dotnet csharpier . - -# Then run dotnet format to apply .editorconfig rules -dotnet format - -# Verify no changes needed -dotnet format --verify-no-changes -``` - -#### Key Formatting Rules (from .editorconfig) - -- **No `var` keyword**: Use explicit types everywhere - - ```csharp - // ✅ CORRECT - string text = "hello"; - List numbers = []; - - // ❌ WRONG - var text = "hello"; - var numbers = new List(); - ``` - -- **Indentation**: 4 spaces (not tabs) -- **Line endings**: CRLF (Windows) -- **Charset**: UTF-8 -- **Final newline**: Required -- **Trailing whitespace**: Not allowed -- **File-scoped namespaces**: Required -- **Collection expressions**: Preferred `[]` over `new List()` - -#### Pre-Commit Hook - -The Husky.Net pre-commit hook automatically runs: - -1. `dotnet csharpier .` - Code formatting -2. `dotnet format` - Style enforcement - -**Commits will be rejected if formatting fails!** - -### .NET 10 and AOT Considerations - -- The project targets .NET 10 with PublishAot enabled -- Avoid reflection where possible (not AOT-friendly) -- Use source generators instead of runtime reflection when applicable -- Be mindful of trim warnings and compatibility -- Ensure all code is AOT-compatible -- Test AOT compatibility with `dotnet publish` - -## Project Structure - -- **Utilities/**: Main library project containing utility classes - - `CommandLineEx.cs`: Command-line argument parsing utilities - - `ConsoleEx.cs`: Console interaction helpers with color support - - `Download.cs`: HTTP download utilities (sync and async) - - `Extensions.cs`: Extension methods (string compression, logger error handling) - - `FileEx.cs`: File and directory operation utilities with retry logic (sync and async) - - `FileExOptions.cs`: Configuration options for FileEx operations - - `Format.cs`: Byte size formatting utilities (binary and decimal) - - `LogOptions.cs`: Global logging configuration - - `StringCompression.cs`: String compression/decompression using Deflate (sync and async) - - `StringHistory.cs`: Bounded string history buffer - -- **Sandbox/**: Console application for testing and experimentation -- **UtilitiesTests/**: Unit tests using xUnit - -## Testing - -- Use xUnit for all tests -- Follow AAA pattern (Arrange, Act, Assert) -- Test file names should match the class being tested with "Tests" suffix -- Run tests frequently during development -- Maintain good test coverage for public APIs -- **Add tests for all new async methods** -- Consider edge cases and error conditions in tests - -## Dependencies - -- **Serilog**: Logging framework (required for LogOptions and error handling) -- **Microsoft.SourceLink.GitHub**: Source linking for debugging -- **xUnit**: Testing framework -- Keep dependencies minimal and well-justified -- Update package references to latest stable versions when appropriate - -## Common Tasks - -### Building - -Run the ".NET Build" task or use: `dotnet build` - -### Publishing - -Run the ".NET Publish" task or use: `dotnet publish` - -### Formatting (REQUIRED before commit) - -```bash -# Step 1: Format with CSharpier -dotnet csharpier . - -# Step 2: Apply dotnet format rules -dotnet format - -# Step 3: Verify (this is what pre-commit hook checks) -dotnet format --verify-no-changes -``` - -### Running Tests - -Use: `dotnet test` - -## Commit Guidelines - -### Versioning - -`develop` leads `main` by a minor. After a `develop -> main` release lands and main's publish completes, bump the minor in [version.json](../version.json) on `develop` via an isolated `bump-version-X.Y` PR (X.Y = the new minor, e.g. `bump-version-3.5`; this version-setting PR is the one place a version belongs in a PR title), so develop's NBGV prereleases sort above main's last stable. A `develop -> main` promotion that carries only maintenance (dependency bumps, CI/doc fixes, template re-syncs) holds main's version instead - `git checkout main -- version.json` on the promotion branch. See [AGENTS.md "Release Model"](../AGENTS.md#release-model). - -### Pre-Commit Process (Automated by Husky.Net) - -The following happens automatically on every commit: - -1. ✅ CSharpier formats all C# files -2. ✅ dotnet format applies .editorconfig rules -3. ✅ Commit proceeds if formatting passes -4. ❌ Commit is rejected if formatting fails - -### Manual Pre-Commit Checklist - -Before committing, ensure: - -- [ ] Code formatted with CSharpier (`dotnet csharpier .`) -- [ ] Style rules applied (`dotnet format`) -- [ ] No formatting issues (`dotnet format --verify-no-changes`) -- [ ] All tests passing (`dotnet test`) -- [ ] Build successful (`dotnet build`) -- [ ] No `var` keywords used -- [ ] XML documentation complete -- [ ] Commit message is clear and descriptive - -### Commit Message Format - -Follow conventional commit format: - -```text -(): - -[optional body] - -[optional footer] -``` - -Examples: - -- `feat(download): add async download methods` -- `fix(fileex): correct boundary condition in DeleteDirectory` -- `docs(readme): update async method examples` -- `test(compression): add async compression tests` - -## Package Information - -- **Package ID**: InsaneGenius.Utilities -- **Namespace**: InsaneGenius.Utilities -- **License**: MIT -- **Repository**: -- **Target Framework**: .NET 10 -- **C# Version**: 14.0 -- **Version**: 3.5 (managed by Nerdbank.GitVersioning) - -## When Adding New Features - -1. **Consider AOT compatibility from the start** -2. **Add comprehensive XML documentation** (required for all public APIs) -3. **Create corresponding unit tests** (including async versions) -4. Update README.md if adding significant functionality -5. Ensure backward compatibility when modifying existing APIs -6. Consider performance implications -7. **Use async/await for I/O-bound operations** with proper cancellation token support -8. Handle exceptions appropriately with logging via LogOptions.Logger -9. **Follow existing patterns** (e.g., retry logic, bool return values, exception handling) -10. **Format with CSharpier before running dotnet format** - -## Code Generation Preferences - -### Modern C# Features (C# 14) - -- Prefer modern C# language features (pattern matching, records, file-scoped namespaces, etc.) -- Use nullable reference types consistently with `ArgumentNullException.ThrowIfNull()` -- Leverage expression-bodied members where appropriate -- Use collection expressions `[]` for initialization -- Use `extension` keyword for extension methods (inside static class) -- Prefer LINQ for data transformations -- Use primary constructors where appropriate - -### Async/Await Patterns - -- **Always use `ConfigureAwait(false)` in library code** -- Provide async versions of I/O-bound methods -- Use `CancellationToken` parameters (default to `default`) -- Use `await using` for async disposal -- Replace blocking calls (`.GetAwaiter().GetResult()`) with proper async -- Use `Task.Delay()` instead of `Thread.Sleep()` in async methods -- Use `Memory` and `Span` for async I/O operations - -### Input Validation - -- Use `ArgumentNullException.ThrowIfNull()` for null checks -- Validate parameters early in methods -- Document all exceptions in XML comments - -### Resource Management - -- Use `using` statements for proper disposal -- Use `await using` for async disposal -- Avoid explicit `.Close()` calls (using handles it) -- Use `leaveOpen` parameter when appropriate - -### Thread Safety - -- Use `Lazy` for thread-safe initialization -- Use `Lock` (C# 13+) instead of `object` for locks -- Avoid static mutable state -- Document thread-safety guarantees - -## Security Considerations - -- Validate all user inputs -- Use secure defaults -- Avoid hardcoding sensitive information -- Follow principle of least privilege -- Use secure random number generation when needed -- Be careful with file path manipulation - -## Performance Guidelines - -- Profile before optimizing -- Be mindful of allocations -- Use `Span` and `Memory` for performance-critical code -- Consider using object pooling for frequently allocated objects -- Use `ValueTask` for async methods that may complete synchronously -- Leverage AOT benefits for startup time and memory usage -- Avoid unnecessary string allocations -- Use `StringBuilder` for string concatenation in loops - -## Common Patterns in This Project - -### Error Handling - -```csharp -try -{ - // Operation -} -catch (IOException e) when (LogOptions.Logger.LogAndHandle(e)) -{ - // Retry or return false -} -catch (Exception e) when (LogOptions.Logger.LogAndHandle(e)) -{ - return false; -} -``` - -### Retry Logic - -- Use `Options.RetryCount` for retry attempts -- Use `Options.Cancel.IsCancellationRequested` for cancellation -- Use `Task.Delay()` for async waits -- Log retry attempts with `LogOptions.Logger.Information()` - -### Method Signatures - -- I/O methods return `bool` for success/failure -- Async methods have `Async` suffix -- Async methods include optional `CancellationToken cancellationToken = default` -- Use `out` parameters for additional return values - -### XML Documentation - -- Always include `` for all public members -- Document all `` with descriptions -- Document `` with descriptions -- Document all possible `` types -- Use `` for additional context -- Reference other types with `` - -## Anti-Patterns to Avoid - -❌ Sync-over-async: `.GetAwaiter().GetResult()`, `.Wait()`, `.Result` -❌ Missing `ConfigureAwait(false)` in library code -❌ Missing XML documentation on public APIs -❌ Not using `ArgumentNullException.ThrowIfNull()` -❌ Explicit `.Close()` calls when using `using` -❌ Missing cancellation token support in async methods -❌ Race conditions in static initialization -❌ Reflection (not AOT-compatible) -❌ Missing tests for async methods -❌ Using `var` keyword (explicit types required by .editorconfig) -❌ Forgetting to run CSharpier before dotnet format - -## File-Specific Notes - -### Download.cs - -- Uses thread-safe `Lazy` initialization -- Provides both sync and async versions -- Returns tuples from async methods for multiple values -- Uses `HttpCompletionOption.ResponseHeadersRead` for efficiency - -### FileEx.cs - -- All I/O methods have async versions -- Uses `Options` for retry configuration -- Returns `bool` for success/failure -- Supports cancellation via `Options.Cancel` and method parameter - -### StringCompression.cs - -- Supports configurable compression levels -- Has both sync and async versions -- Uses `leaveOpen` for stream management -- Proper error documentation - -### Extensions.cs - -- Uses C# 14 `extension` keyword -- Must be inside static class -- Provides extension methods for string compression and logger error handling - -## Development Workflow - -### Making Changes - -1. Edit code -2. **Run CSharpier**: `dotnet csharpier .` -3. **Run dotnet format**: `dotnet format` -4. Build: `dotnet build` -5. Test: `dotnet test` -6. Commit (Husky.Net pre-commit hook will verify formatting) - -### Before Committing - -```bash -# Format code (REQUIRED ORDER) -dotnet csharpier . -dotnet format - -# Verify -dotnet format --verify-no-changes -dotnet build -dotnet test - -# Commit -git add . -git commit -m "feat: your message" -# Husky.Net hook runs automatically -``` - -### If Pre-Commit Hook Fails - -```bash -# Hook will show formatting errors -# Re-run formatters -dotnet csharpier . -dotnet format - -# Try commit again -git commit -m "feat: your message" -``` - -## Tools Required - -- .NET 10 SDK -- CSharpier (installed as dotnet tool) -- Husky.Net (installed as dotnet tool) -- Visual Studio 2022 or VS Code with C# extension - -## EditorConfig Integration - -The project uses `.editorconfig` for style enforcement. Key rules: - -- `csharp_style_var_*` = **false** (no var keyword) -- `csharp_style_namespace_declarations` = **file_scoped** -- `csharp_prefer_system_threading_lock` = **true** -- `dotnet_style_prefer_collection_expression` = **when_types_loosely_match** - -Visual Studio and Rider automatically apply these settings. VS Code requires the EditorConfig extension. +- Imperative subject, <= 72 characters, no trailing period; optional blank-line-separated body for the non-obvious *why*. +- US English, title case with lowercase short bind words; no vague titles, no `Co-Authored-By:` unless asked, no release-bump magnitude (NBGV handles versioning). Dependabot's `Bump X from Y to Z` titles are fine. +- develop PRs squash-merge (`gh pr merge --squash`), main PRs merge-commit (`--merge`); a mismatched flag is rejected by branch protection. ## GitHub Copilot Review Runbook +> This runbook implements the [AGENTS.md "PR Review Etiquette"](../AGENTS.md#pr-review-etiquette) review-loop contract for GitHub Copilot. Without it in-repo, an agent has no pointer to the reliable Copilot mechanics and falls back to known-broken paths (the no-op `POST /requested_reviewers`, the wrong bot-login filter). + Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md -> PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. ### Triggering and Polling -Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off). +Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This drives the loop end-to-end without a UI hand-off. + +**A review with no inline comments is still a completed review - not a failure, and not a reason to ask the maintainer to re-trigger.** Copilot very often posts a single formal review (GraphQL `state: COMMENTED`) whose body ends with "...reviewed N of N changed files ... and generated no comments" and adds **zero** inline threads. That review carries the head `commit.oid` and fully satisfies the loop - it is the clean-pass success case. Never read "no inline comments" as "the review didn't run," and never re-request or escalate to the maintainer because comments are absent. -> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. +**Round 1 is normally auto-seeded - poll for it before trying to self-trigger.** Auto-review-on-open supplies the first review with no `botIds` call needed, but it can lag one to three minutes. After opening a PR (or the first push), **poll** for a Copilot review on the head SHA (see [Verify Review Covered Current Head](#verify-review-covered-current-head)) before concluding none ran. The `requestReviews` mutation below is for **re-requesting on later pushes** (a new head SHA); by then a prior review exists, so its bot node id is readable. A missing bot node id on round 1 therefore means "the auto-review has not landed yet - wait and poll," **not** "ask the maintainer to kick it off." + +> **The reviewer login differs by API.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them. ```sh # 1. PR node id + the Copilot reviewer's bot node id (read from any existing @@ -447,7 +56,7 @@ mutation($pr: ID!, $bot: ID!) { }' -F pr="$PR_NODE" -F bot="$BOT_ID" ``` -The bot node id is read from an existing Copilot review, so step 1 needs at least one prior review on the PR - the auto-review-on-open normally supplies the first one. If no Copilot review exists yet and auto-review didn't fire, request `Copilot` once through the GitHub PR UI to seed it, then use the mutation for every subsequent re-request. +The bot node id is read from an existing Copilot **formal** review (`pullRequest.reviews`), so step 1 needs at least one prior formal review on the PR - the auto-review-on-open normally supplies the first one (it may have **no inline comments**; that still counts, and its bot node id is still readable). Poll for it (give auto-review-on-open a few minutes) before deciding it is missing. If Copilot posted **only an issue comment** and no formal review, the head is covered but `reviews` yields no bot node id - read the id from the Copilot issue comment's author by querying the PR's issue comments in GraphQL (`pullRequest.comments` -> author `... on Bot { id }`), or request `Copilot` once through the GitHub PR UI to produce a formal review. Manual UI seeding is the fallback specifically when no formal review exists to read the id from; then use the mutation for every subsequent re-request. **Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. @@ -474,10 +83,12 @@ gh api repos/ptr727/Utilities/issues//comments --jq \ '[.[] | select(.user.login=="copilot-pull-request-reviewer[bot]")] | last | {created_at, body: .body[:200]}' ``` -Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. +Coverage is confirmed when (1) exits 0 - **a formal review with no inline comments still satisfies path (1)**, because coverage is about the head SHA, not the comment count. For issue comments (path 2), body content is the only reliable signal - `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. ### Bounded Retry Workflow +This path is only for a **genuinely missing** review - no Copilot review (formal *or* issue comment) covers the current head SHA after polling. A review that covered the head but produced no comments is a clean pass, not a missing review; do not enter this retry path for it. + If a review did not run on the current head, retry: 1. Wait briefly and check head-SHA coverage (see above). @@ -531,7 +142,13 @@ Issue-level Copilot comments (those in `issues//comments`) have no resolution Reply-body conventions: - Accepted bug/style fix: include fixing commit SHA and a one-line summary. -- Declined style comment: cite the rule (AGENTS.md or language CODESTYLE) and the existing-tree precedent. +- Declined style comment: cite the rule (AGENTS.md or CODESTYLE.md) and the existing-tree precedent. - Declined architecture proposal: one-sentence rationale. After the final push, sweep-resolve stale older threads for removed code paths. + +## When in Doubt + +Read [AGENTS.md](../AGENTS.md) for this repo's conventions. For language-specific rules, [`CODESTYLE.md`](../CODESTYLE.md) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. + +**In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream). diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml index 16197f6..9a77094 100644 --- a/.github/workflows/build-nugetlibrary-task.yml +++ b/.github/workflows/build-nugetlibrary-task.yml @@ -91,3 +91,4 @@ jobs: with: name: nugetlibrary-build-${{ inputs.branch }} path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} + retention-days: 1 diff --git a/AGENTS.md b/AGENTS.md index f7a860c..12d3848 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,19 +1,21 @@ # Instructions for AI Coding Agents -**Utilities** is a C# .NET NuGet library (published as `InsaneGenius.Utilities`). The library ships under [`Utilities/`](./Utilities/), with a `Sandbox/` console app for experimentation and `UtilitiesTests/` for xUnit tests. This file is the cross-cutting source of truth for process rules; the C# style guidance lives in [`.github/copilot-instructions.md`](./.github/copilot-instructions.md). +**Utilities** is a C# .NET NuGet library (published as `InsaneGenius.Utilities`). The library ships under [`Utilities/`](./Utilities/), with a `Sandbox/` console app for experimentation and `UtilitiesTests/` for xUnit tests. This file is the cross-cutting source of truth for process rules and this repo's project-specific conventions and public-API contracts; the C# style guidance lives in [`CODESTYLE.md`](./CODESTYLE.md). This repo tracks the [ptr727/ProjectTemplate](https://github.com/ptr727/ProjectTemplate) two-phase release model. It is a **NuGet-only** derivation: it has no Docker, executable, PyPI, or codegen targets, so the template's `build-docker-task.yml`, `build-executable-task.yml`, `build-pypilibrary-task.yml`, and `run-codegen-*.yml` workflows are intentionally absent, and the merge-bot carries only the Dependabot path. Keep the remaining workflow filenames and structure aligned with the template so upstream changes apply as minimal deltas. ## Git and Commit Rules - **Default to staging, not committing.** Stage changes with `git add` and leave `git commit` to the developer unless explicitly authorized for the current task. Authorization is scope-bound to that task. +- **All commits must be cryptographically signed (SSH or GPG).** Branch protection enforces this on both branches; unsigned commits are rejected on push. Signing depends on environment configuration (`git config commit.gpgsign true`, a configured `user.signingkey`, and a working signing agent). If signing is not configured, **do not commit** - surface the missing config to the developer and stop at `git add`. - **Never force push** (`git push --force` / `--force-with-lease`) and **never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. ## Branching Model -- `develop` is the integration branch. Feature branches → `develop` is **squash-only**; develop is kept linear. -- `develop` → `main` is **merge-commit only** (no squash, no rebase). Merge commits preserve develop's commit list as a real second-parent reference on main. -- **`develop` is forward-only - no `main → develop` back-merges.** Each branch absorbs its own Dependabot PRs directly. +- `develop` is the integration branch. Feature branches -> `develop` is **squash-only**; develop is kept linear. +- `develop` -> `main` is **merge-commit only** (no squash, no rebase). Merge commits preserve develop's commit list as a real second-parent reference on main. +- All commits on both branches must be cryptographically signed (SSH or GPG). Squash and merge commits created via the GitHub UI are signed by GitHub's web-flow key. +- **`develop` is forward-only - no `main -> develop` back-merges.** Each branch absorbs its own Dependabot PRs directly. - **Both branch rulesets intentionally omit "Require branches to be up to date before merging".** On `main` the graph-based check would fail on every release (main's new merge commit is never back-merged into develop); on `develop` it stalls bot auto-merge when two bot PRs land in the same window. - **Dependabot targets both `main` and `develop` in parallel.** [`.github/dependabot.yml`](./.github/dependabot.yml) duplicates every ecosystem entry (one per branch). The merge-bot ([`.github/workflows/merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml)) dispatches `--squash` or `--merge` from each PR's base ref via a `case` statement so the form matches the ruleset on either base. Dependabot **security** PRs always open against the default branch (`main`) - the same `case` statement covers them. - **Maintainer-pushed commits on a bot PR auto-disable auto-merge.** The merge-bot's `merge-dependabot` job only fires on `opened` / `reopened` (auto-merge is enabled once per PR); the `disable-auto-merge-on-maintainer-push` job disables it on a `synchronize` event whose actor isn't Dependabot. Re-enable manually when ready. @@ -27,8 +29,10 @@ The repo uses a **two-phase model by default**: PRs build fast, publishing is ba - **Merges don't publish by default.** [`publish-release.yml`](./.github/workflows/publish-release.yml) is the sole publisher: its **weekly schedule** (Mondays 02:00 UTC) and **manual `workflow_dispatch`** always do the full build/publish of **both** `main` and `develop` (a branch matrix). Its `push` trigger publishes only when the **`PUBLISH_ON_MERGE` repository variable** is `true` (opt-in continuous-release). Unset/`false` = two-phase. - **Idempotent weekly republish.** NBGV can produce the same `SemVer2` on an unchanged branch, so the GitHub release step is skipped when the tag already exists, and the NuGet push uses `--skip-duplicate` - an unchanged week is a no-op. - **Required check.** The `changes` job is in the `Check pull request workflow status` aggregator's `needs` and **must succeed** (not just "not fail") so a paths-filter error can never let a library-changing PR merge with its smoke build silently skipped. Skipped smoke jobs (no matching change) pass; `failure`/`cancelled` blocks. -- **Reusable-task parameter contract.** Every `build-*-task.yml` and `build-release-task.yml` takes `ref` (git ref to check out/version), `branch` (logical branch driving config/tags/prerelease - `main` ⇒ Release/non-prerelease, else Debug/prerelease), and where relevant `smoke`. **Branch-derived config keys off `inputs.branch`, never `github.ref_name`** - the publisher's matrix builds `develop` from a run whose `github.ref_name` is `main`. Artifact names are branch-suffixed so both matrix legs coexist in one run. -- **Versioning is semantic and maintainer-controlled.** The `version` (major.minor) in [`version.json`](./version.json) is the version floor; NBGV appends the git height (the SemVer patch position). `main` builds a stable `X.Y.`; `develop` builds a prerelease `X.Y.-g`. **`develop` leads `main` by a minor:** after a `develop -> main` release lands and main's publish completes, bump the minor in `version.json` on `develop` in an isolated `bump-version-X.Y` PR (X.Y = the new minor, e.g. `bump-version-3.5`; this version-setting PR is the one place a version belongs in a PR title), so develop's prereleases sort above main's last stable (at a shared `X.Y`, develop's `X.Y.-g` sorts *below* main's `X.Y.`). A **maintenance** `develop -> main` promotion (dependency bumps, CI/doc fixes, template re-syncs) holds main's version - `git checkout main -- version.json` on the promotion branch - so `main` advances only its height, not its minor. +- **Reusable-task parameter contract.** Every `build-*-task.yml` and `build-release-task.yml` takes `ref` (git ref to check out/version), `branch` (logical branch driving config/tags/prerelease - `main` => Release/non-prerelease, else Debug/prerelease), and where relevant `smoke`. **Branch-derived config keys off `inputs.branch`, never `github.ref_name`** - the publisher's matrix builds `develop` from a run whose `github.ref_name` is `main`. Artifact names are branch-suffixed so both matrix legs coexist in one run. +- **Versioning is semantic and maintainer-controlled.** The `version` (major.minor) in [`version.json`](./version.json) is the version floor; NBGV appends the git height (the SemVer patch position) for the build version. `main` (the public release ref) builds a stable `X.Y.`; `develop` builds a prerelease `X.Y.-g`. The maintainer edits `version.json`; dependency bumps, CI/workflow fixes, doc edits, and template re-syncs leave it untouched. + - **Bump `version.json` only for functional changes, by maintainer instruction.** Raise the major/minor when the work being introduced warrants a new semantic version - a new feature, a behavior or API change, a breaking change - and do it in the PR that introduces that work (typically on `develop`). Do **not** bump on a fixed cadence or mechanically after a release. NBGV advances the patch (git height) on every commit automatically, so a release always gets a fresh build version without any `version.json` edit. + - **No post-release bump; no develop-ahead requirement.** NBGV advances the patch (git height) on every commit, so a release always gets a fresh build version with no `version.json` edit and there is no `bump-version-X.Y` PR after a release. A `develop -> main` promotion carries whatever `version.json` is current: a promotion with a functional bump releases that new version on `main`; a maintenance-only promotion carries the unchanged `version.json` and `main` advances only its NBGV height. ## Build Configuration @@ -36,24 +40,63 @@ The repo uses a **two-phase model by default**: PRs build fast, publishing is ba - **Versioning.** Nerdbank.GitVersioning reads [`version.json`](./version.json); only `main` is a public release ref. Don't put release-bump magnitude in PR titles - NBGV computes the next version from git history. - **Analyzer relaxations.** `Directory.Build.props` mirrors the template's strict `AnalysisLevel latest-all` / `AnalysisMode All` / `TreatWarningsAsErrors`. Because this is a pre-existing (brownfield) library, a specific set of rules that would otherwise break the build - or require breaking the published public API - are relaxed back to suggestion in [`.editorconfig`](./.editorconfig) (and `IL3058` via `NoWarn` in the AOT project files). Each relaxation is documented inline; prefer fixing new violations over adding new relaxations. -## Workflow YAML Conventions +## Pull Request Title and Commit Message Conventions -- **Action pinning**: pin **every** action to a commit SHA with a trailing `# vX.Y.Z` comment. Documented exception: [`dotnet/nbgv`](./.github/workflows/get-version-task.yml) is consumed via `@master` because the upstream tag stream lags `master` and Dependabot would propose a downgrade. -- **Filename**: reusable workflows (`on: workflow_call`) end in `-task.yml`; entry-point workflows do not use the `-task` suffix. -- **Workflow `name:`**: reusable workflow names end in **"task"**; entry-point names end in **"action"**. -- **Job and step `name:`**: every job's `name:` ends in **"job"**; every step's `name:` ends in **"step"**. **Exception**: the ruleset-bound required-status-check job `Check pull request workflow status` in `test-pull-request.yml` keeps its name verbatim - renaming silently breaks required-status-check enforcement. -- **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'`, `cancel-in-progress: true`. Documented exceptions: `merge-bot-pull-request.yml` (`cancel-in-progress: false`, to run enable/disable events to completion in arrival order) and `publish-release.yml` (global ref-independent group + `cancel-in-progress: false`, so scheduled and manual publishes serialize instead of double-publishing). -- **Shells**: multi-line bash `run:` blocks start with `set -euo pipefail`. -- **Conditionals**: multi-line `if:` uses folded scalar `if: >-`. -- **Tag pinning on releases**: pass `target_commitish` to `softprops/action-gh-release` explicitly, pinned to NBGV's `GitCommitId` (the exact built commit), not `github.sha` or a branch name. -- There is no CI workflow-lint job - lint workflow edits with `actionlint` locally before pushing. +### Format -## Pull Request Title and Commit Message Conventions +- Imperative subject summarizing the change, <=72 characters, no trailing period. ("Add async download overloads", not "Added X" or "Adds X".) +- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*. -- Imperative subject summarizing the change, ≤72 characters, no trailing period. -- Don't write vague titles (`update stuff`, `wip`). Dependabot's default `Bump X from Y to Z` titles are fine. +### Rules + +- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.) - Don't add `Co-Authored-By:` lines unless the developer explicitly asks. -- Use US English spelling. +- Don't put release-bump magnitude in the title - no "minor", "patch", "release v3.5", etc. Nerdbank.GitVersioning computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected. +- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from). + +### Examples + +```text +Add structured logging extensions to library +Pin softprops/action-gh-release to commit SHA +Drop ProcessEx wrapper in favor of CliWrap +Bump xunit.v3 from 3.2.2 to 3.3.0 +Clarify release model in README +``` + +## Documentation Style Conventions + +### Markdown + +- Use reference-style links for any URL referenced more than once or appearing in lists; alphabetize the reference definitions block. Inline single-use relative links (e.g. `[CODESTYLE.md](./CODESTYLE.md)`) are fine. +- One logical paragraph per line; no hard-wrap line-length limit. For an intentional hard line break within a block - stacked badges, status, or license lines - end the line with a trailing backslash (`\`); this explicit form is preferred over trailing whitespace and is not treated as a paragraph split. +- Headings follow the title-case-with-short-bind-words rule from the PR-title section. +- **Write docs in the current state, not as a change from a prior one.** Describe what *is*: "X does Y", never "X *now* does Y" or "changed/switched to Y". Before/after framing belongs in changelogs, commit messages, and PR descriptions - not in `README.md` or other living docs. + +### Comments + +Applies to code and workflow (`#`) comments alike. + +- Comment only when the code does not explain itself or the logic is genuinely complex. Self-evident code needs no comment. +- Write for the human reading *this* project's code now: state what the code does and only the non-obvious *why*. No cross-project references (do not name other repos), no historic or design narrative, no rule citations - governance lives in this file, not echoed inline. +- Match the surrounding code's line length (typically ~120), not an 80-column wrap. + +### Character Set + +- **Write ASCII in all agent-authored text** - documentation, code, comments, commit messages, and PR descriptions. Replace typographic Unicode with its ASCII equivalent on sight: + - em dash and en dash -> hyphen `-` (use a spaced ` - ` for an em-dash-style clause break) + - right arrow -> `->`; double arrow -> `=>`; `<=` and `>=` for the inequality symbols + - curly quotes -> straight `'` and `"`; ellipsis -> `...` +- **Allowed non-ASCII**: scientific/technical symbols with no clean ASCII equivalent (ohm, micro, degree, pi), and Unicode the developer deliberately typed (emoji callout markers in `README.md`). Preserve those; never strip the developer's own characters. + +### Line Endings + +- **[`.editorconfig`](./.editorconfig) defines the correct line ending per file type:** **CRLF** for `.md`, `.cs`, XML/`.csproj`/`.props`/`.targets`, `.yml`/`.yaml`, `.json`, and `.cmd`/`.bat`/`.ps1`; **LF** for `.sh`. `.gitattributes` is `* -text`, so git stores the exact bytes you commit and will **not** normalize endings for you. +- **New files:** create them with the `.editorconfig`-mandated ending. **Editing an existing file:** preserve the file's current line endings - do not reflow them as a side effect of a content change. After any programmatic edit, verify with `git diff --stat` that only the lines you changed are touched; if a diff balloons to the whole file, you flipped the endings - restore them and re-stage. + +### Quantitative Claims + +- Any quantitative claim in `README.md` (counts, sizes, version floors, supported platforms) must be verified against current code. If a doc number is derived from a code constant, mark the dependency in a source-code comment so the next editor knows to update both. ## PR Review Etiquette @@ -99,6 +142,76 @@ Bring the user in when: Anti-pattern: don't keep flipping the code on the same style point. Flip the rule once and stick to the rule. +## Workflow YAML Conventions + +- **Action pinning**: pin **every** action to a commit SHA with a trailing `# vX.Y.Z` comment. Documented exception: [`dotnet/nbgv`](./.github/workflows/get-version-task.yml) is consumed via `@master` because the upstream tag stream lags `master` and Dependabot would propose a downgrade. +- **Filename**: reusable workflows (`on: workflow_call`) end in `-task.yml`; entry-point workflows do not use the `-task` suffix. +- **Workflow `name:`**: reusable workflow names end in **"task"**; entry-point names end in **"action"**. +- **Job and step `name:`**: every job's `name:` ends in **"job"**; every step's `name:` ends in **"step"**. **Exception**: the ruleset-bound required-status-check job `Check pull request workflow status` in `test-pull-request.yml` keeps its name verbatim - renaming silently breaks required-status-check enforcement. +- **Concurrency**: top-level workflows use `group: '${{ github.workflow }}-${{ github.ref }}'`, `cancel-in-progress: true`. Documented exceptions: `merge-bot-pull-request.yml` (`cancel-in-progress: false`, to run enable/disable events to completion in arrival order) and `publish-release.yml` (global ref-independent group + `cancel-in-progress: false`, so scheduled and manual publishes serialize instead of double-publishing). +- **Shells**: multi-line bash `run:` blocks start with `set -euo pipefail`. +- **Conditionals**: multi-line `if:` uses folded scalar `if: >-`. +- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts. +- **Tag pinning on releases**: pass `target_commitish` to `softprops/action-gh-release` explicitly, pinned to NBGV's `GitCommitId` (the exact built commit), not `github.sha` or a branch name. +- There is no CI workflow-lint job - lint workflow edits with `actionlint` locally before pushing. + +### Running the Linters Locally (Known-Working Invocations) + +There is no CI lint job for workflow YAML or Markdown - the gate is local. Prefer the Docker invocations below; they need no local toolchain and auto-discover their targets from the working directory. + +- **actionlint** (run after any `.github/workflows/` edit, since workflow-only changes are not smoke-built): + + ```sh + docker run --rm -v "$PWD":/repo --workdir /repo rhysd/actionlint:latest -color + ``` + + The `rhysd/actionlint` image bundles `shellcheck`, so it also validates `run:` shell blocks. + +- **markdownlint-cli2** (mirrors the davidanson VS Code extension via the shared [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc)): + + ```sh + docker run --rm -v "$PWD":/workdir davidanson/markdownlint-cli2:latest "**/*.md" + ``` + +When pulling a public image fails on a Docker-Desktop/WSL credential-helper error (`docker-credential-desktop.exe: exec format error`), retry with an empty Docker config: `DOCKER_CONFIG=$(mktemp -d) docker run ...` after writing `{}` to `$DOCKER_CONFIG/config.json`. + +## Project Structure + +- **.NET projects** (build with `dotnet build`, test with `dotnet test`): + - `Utilities/` - the reusable .NET NuGet library (published as `InsaneGenius.Utilities`) + - `Sandbox/` - console app for experimentation + - `UtilitiesTests/` - xUnit tests + - **Style guide: [`CODESTYLE.md`](./CODESTYLE.md)**. +- **Cross-cutting**: + - `.github/` - workflows, Dependabot, Copilot instructions + - `.vscode/` - debug configs and tasks + +## Library API Conventions + +Project-specific public-API conventions for the library (these are behavioral contracts, so they live here rather than in `CODESTYLE.md`): + +- **I/O methods return `bool`** for success/failure; additional outputs use `out` parameters. +- **Async methods carry the `Async` suffix** and an optional `CancellationToken cancellationToken = default`, passed through to the underlying call. +- **`Download`** reuses a thread-safe `Lazy` and uses `HttpCompletionOption.ResponseHeadersRead`; async overloads return tuples for multiple values. +- **`FileEx`** wraps I/O in retry logic configured via `Options`, with cancellation via `Options.Cancel` and the method parameter. +- **`StringCompression`** uses Deflate, supports configurable compression levels, and passes `leaveOpen` so the caller retains stream ownership. +- **`Extensions`** uses the C# `extension` syntax (inside a static class) for logger and string helpers. + +## Files and Sections Derived Repos Must Carry Verbatim + +These artifacts are the template's cross-cutting contract; this repo carries each of them. Re-sync them from the template when it changes, adapting only the noted placeholders. + +- **[`AGENTS.md`](./AGENTS.md) "PR Review Etiquette" section** - the provider-agnostic review-loop contract. Carried verbatim (it names no owner/repo). +- **[`.github/copilot-instructions.md`](./.github/copilot-instructions.md)** - the whole file is a drop-in; its "GitHub Copilot Review Runbook" carries the provider mechanics. Only the `` / `` placeholders are adapted (to `ptr727` / `Utilities`). Keep it **narrow** - provider mechanics plus the inline commit/PR-title summary; project-specific conventions and API contracts belong in this file instead. +- **[`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc)** - the shared lint config read by both the davidanson `markdownlint` IDE extension and CLI `markdownlint-cli2`, so the IDE and command line stay in lock-step. Carried verbatim (it is repo-agnostic). +- **[`.editorconfig`](./.editorconfig) and [`.gitattributes`](./.gitattributes)** - line-ending governance. The defaults + per-extension EOL block is always-verbatim; the `[*.cs]` + ReSharper style block at the end is .NET-only (the file marks the boundary). + +## Staying in Sync and Reporting Drift Upstream + +This repo re-syncs against [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) periodically, not just at creation: pull the current version of each verbatim-carry artifact above and re-apply it (adapting only the noted placeholders). For [`CODESTYLE.md`](./CODESTYLE.md), re-sync from the template's .NET aggregate and adapt it to this repo's projects. + +**Drift flows back upstream as an issue, not a private fix.** When re-syncing, if you find a discrepancy that should be fixed in the **template itself** - a gap, an outdated instruction, a missing rule, something that bit this repo and would bite the next derived repo too - **open an issue in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate)** describing it, rather than only patching it locally. A local fix realigns *this* repo; an upstream issue (then fix) corrects it for every future derived repo and keeps the template the single source of truth. This upstream-issue rule is this repo's sole cross-repo obligation: do not name sibling or downstream repos in this repo's docs, comments, or AGENTS - a reader here cares only about this project. + ## Maintainer Setup (GitHub) - **Secrets**: `NUGET_API_KEY` (NuGet.org push); `CODEGEN_APP_CLIENT_ID` + `CODEGEN_APP_PRIVATE_KEY` for the merge-bot's GitHub App token - add these to **both** the Actions and Dependabot secret stores. diff --git a/CODESTYLE.md b/CODESTYLE.md new file mode 100644 index 0000000..7324c76 --- /dev/null +++ b/CODESTYLE.md @@ -0,0 +1,315 @@ +# Code Style and Formatting Rules - .NET + +This file is the style guide for the .NET projects in this repo: [`Utilities/`](./Utilities/) (the published library), [`Sandbox/`](./Sandbox/) (a console app for experimentation), and [`UtilitiesTests/`](./UtilitiesTests/) (xUnit tests). + +Cross-cutting rules (PR titles, branching, US English, markdown style, workflow YAML, PR review etiquette) live in [AGENTS.md](./AGENTS.md) and apply across the repo. This file only documents what's specific to C# / .NET. + +## Build Requirements + +### Zero Warnings Policy + +**CRITICAL**: All builds must complete without warnings. The project enforces this through: + +1. **VS Code tasks** + - `CSharpier Format` -> `.NET Build` -> `.NET Format` + - `.NET Format` must pass with `--verify-no-changes` before commit + - Command: `dotnet format --verify-no-changes --severity=info --verbosity=detailed` + +2. **Analyzer configuration** + - `latest-all` + - `All` + - `true` + - Because this is a pre-existing (brownfield) library, a specific set of rules are relaxed back to suggestion in [`.editorconfig`](./.editorconfig) (each relaxation documented inline); prefer fixing new violations over adding new relaxations. + +3. **Pre-commit hooks** + - Husky.Net pre-commit hooks are wired in this repo (`.husky/` ships) and run CSharpier and `dotnet format` on every commit; commits are rejected if formatting fails. + +### Build Tasks + +Available VS Code tasks (use via `run_task` tool): + +- `.NET Build`: Build the solution +- `.NET Publish`: Publish the library (exercises AOT) +- `.NET Format`: Verify formatting and style (must pass) +- `CSharpier Format`: Auto-format code with CSharpier +- `.Net Tool Update`: Update dotnet tools +- `Husky.Net Run`: Run the pre-commit hook tasks on demand + +## Tooling and Editor + +### Code Formatting and Tooling + +1. **CSharpier**: Primary code formatter + - Run before committing: `dotnet csharpier format --log-level=debug .` + +2. **dotnet format**: Style verification + - Verify no changes: `dotnet format --verify-no-changes --severity=info --verbosity=detailed` + +3. **Other tools** + - Husky.Net: pre-commit git-hook runner (installed as a dotnet tool) + - Nerdbank.GitVersioning: version management + +The required order is **CSharpier first, then dotnet format**. The Husky.Net pre-commit hook runs `dotnet husky run`, which applies the same checks; restore the tools with `dotnet tool restore` before the first commit. + +### Editor Baseline + +1. **Required VS Code extensions**: CSharpier, EditorConfig, markdownlint, CSpell +2. **VS Code settings**: Use the workspace settings without overrides + +### Markdown Files + +1. **Linting**: All `.md` files must be linted with the VS Code `markdownlint` extension (local only; no CI) +2. **Zero warnings**: Markdown linting must be error and warning free +3. **Authoritative config**: [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc) at the repo root is the single source of truth - the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules the config deliberately disables (e.g. `MD013` line-length, `MD033` inline HTML) are **intentional** - do not "fix" them. + +### Spelling + +1. **CSpell**: All spelling checks must be error free using the CSpell VS Code integration +2. **Accepted spellings**: Words must be correctly spelled in US or UK English +3. **Allowed exceptions**: Project-specific terms must be added to the workspace CSpell config + +## Coding Standards and Conventions + +Note: Code snippets are illustrative examples only. Replace namespaces/types to match the project. + +### C# Language Features + +1. **File-scoped namespaces** + + ```csharp + namespace InsaneGenius.Utilities; + ``` + +2. **Nullable reference types**: Enabled (`enable`) + - Use nullable annotations appropriately + - Use `required` for mandatory properties + +3. **Modern C# features**: Prefer modern language constructs + - Primary constructors when appropriate + - Pattern matching over traditional checks + - Collection expressions when types loosely match + - Extension methods using the `extension` syntax (inside a static class) + - Implicit object creation when type is apparent + - Range and index operators + +4. **Expression-bodied members**: Use for applicable members + - Methods, properties, accessors, operators, lambdas, local functions + +5. **`var` keyword**: Do NOT use `var` (always use explicit types) + + ```csharp + // Correct + int count = 42; + string name = "test"; + + // Incorrect + var count = 42; + var name = "test"; + ``` + +### Naming Conventions + +1. **Private fields**: underscore prefix with camelCase + + ```csharp + private readonly HttpClient _httpClient; + private int _counter; + ``` + +2. **Static fields**: `s_` prefix with camelCase + + ```csharp + private static int s_instanceCount; + ``` + +3. **Constants**: PascalCase + + ```csharp + private const int MaxRetries = 3; + ``` + +### Code Structure + +1. **Global usings**: Use `GlobalUsings.cs` for common namespaces + + ```csharp + global using System; + global using System.Net.Http; + global using System.Threading.Tasks; + global using Serilog; + ``` + +2. **Usings placement**: Outside namespace, sorted with `System` directives first + +3. **Braces**: Allman style + + ```csharp + public void Method() + { + if (condition) + { + // code + } + } + ``` + +4. **Indentation** + - C# files: 4 spaces + - XML/csproj files: 2 spaces + - YAML files: 2 spaces + - JSON files: 4 spaces + +5. **Line endings** + - C#, XML, YAML, JSON, Windows scripts: CRLF + - Linux scripts (`.sh`): LF + +6. **`#region`**: Do not use regions. Prefer logical file/folder/namespace organization. +7. **Member ordering (StyleCop SA1201)**: const -> static readonly -> static fields -> instance readonly fields -> instance fields -> constructors -> public (events -> properties -> indexers -> methods -> operators) -> non-public in same order -> nested types + +### Comments and Documentation + +1. **XML documentation** + - `true` + - Document all public surfaces. + - Single-line summaries, additional details in remarks, document input parameters, return values, exceptions, and add crefs + + ```csharp + /// + /// Example of a single line summary. + /// + /// + /// Additional important details about usage. + /// + /// + /// A that can be used to cancel the request. + /// + /// + /// A indicating success. + /// + /// + /// Thrown when a required argument is null. + /// + public async Task DoWorkAsync(CancellationToken cancellationToken) {} + ``` + +2. **Code analysis suppressions** + - Do not use `#pragma` sections to disable analyzers + - For one-off cases, use suppression attributes with justifications + - For project-wide suppressions, add rules to `.editorconfig` + + ```csharp + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "https://github.com/dotnet/sdk/issues/51681" + )] + ``` + +### Error Handling and Logging + +1. **Serilog logging**: Use structured logging + + ```csharp + logger.Error(exception, "{Function}", function); + ``` + +2. **Library log configuration**: The library exposes logging configuration + - Provide options or settings to supply an `ILogger` + - Offer a global fallback logger for static usage when needed (`LogOptions.Logger`) + +3. **CallerMemberName**: Use for automatic function name tracking + + ```csharp + public bool LogAndPropagate( + Exception exception, + [CallerMemberName] string function = "unknown" + ) + ``` + +4. **Logger extensions**: Use `Extensions.cs` for logger and other extension methods + + ```csharp + extension(ILogger logger) + { + public bool LogAndPropagate(Exception exception, ...) { } + } + ``` + +5. **Exceptions**: Do not swallow exceptions; log and rethrow or translate to a domain-specific exception + + ```csharp + try + { + // Operation + } + catch (IOException e) when (LogOptions.Logger.LogAndHandle(e)) + { + // Retry or return false + } + ``` + +### Code Patterns + +1. **Guard clauses**: Prefer early returns for validation; use `ArgumentNullException.ThrowIfNull()` for null checks +2. **Async all the way**: Avoid blocking calls (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`); use `async`/`await` +3. **Cancellation tokens**: Accept `CancellationToken cancellationToken = default` as the last parameter and pass it through +4. **ConfigureAwait**: In library code, use `ConfigureAwait(false)` unless context is required + - Do not call `ConfigureAwait(false)` in xUnit tests (see xUnit1030) +5. **Disposables**: Use `await using` for async disposables; prefer `using` declarations; pass `leaveOpen` where a caller owns the stream +6. **LINQ vs loops**: Use LINQ for clarity, loops for hot paths or allocations +7. **HTTP**: Reuse `HttpClient` via a thread-safe `Lazy`; avoid per-request instantiation; use `HttpCompletionOption.ResponseHeadersRead` for streaming downloads +8. **Collections**: Prefer `IReadOnlyList`/`IReadOnlyCollection` for public APIs +9. **Immutability**: Prefer immutable records and init-only setters; prefer immutable or frozen collections for read-only data +10. **Sealing classes**: Seal classes that are not designed for inheritance +11. **Thread safety**: Use `Lazy` for static thread-safe instantiation; use `Lock` (C# 13+) instead of `object` for locks; avoid static mutable state and document thread-safety guarantees +12. **Performance**: Use `Span`/`Memory` for performance-critical I/O; use `ValueTask` for async methods that may complete synchronously; use `StringBuilder` for string concatenation in loops + +### Testing Conventions + +1. **Framework**: xUnit + + ```csharp + [Fact] + public void MethodName_Scenario_ExpectedBehavior() + { + // Arrange + int expected = 42; + + // Act + int actual = GetValue(); + + // Assert + Assert.Equal(expected, actual); + } + ``` + +2. **Organization**: Arrange-Act-Assert pattern +3. **Naming**: Descriptive names with underscores; test file names match the class under test with a `Tests` suffix +4. **Theory tests**: Use `[Theory]` with `[InlineData]` +5. **Async coverage**: Add tests for every new async method + +## Project Configuration + +1. **Target framework**: .NET 10.0 (`net10.0`) + +2. **AOT compatibility** + - `true` + - Avoid runtime reflection (not AOT-friendly); prefer source generators + - Be mindful of trim warnings; verify with `dotnet publish` + +3. **Assembly information** + - Use semantic versioning (Nerdbank.GitVersioning) + - Include SourceLink: `true` + - Embed untracked sources: `true` + +4. **Internal visibility**: Use `InternalsVisibleTo` for test access + + ```xml + + + + ``` + +## Best Practices + +1. **Code reviews**: All changes go through pull requests diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..25f8d69 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,18 @@ +# Utilities + +Some useful and not so useful C# .NET utility classes. + +## Release History + +- v3.5: + - Re-synced the repository structure and agent documentation with the upstream ProjectTemplate: added this `HISTORY.md` and a `CODESTYLE.md` .NET style guide, narrowed `.github/copilot-instructions.md` to the Copilot review runbook, and refreshed `AGENTS.md` conventions. + - Corrected the versioning policy to bump `version.json` only for functional changes. + - Swapped the recommended Todo VS Code add-on from Todo Tree to Better Todo Tree. +- v3.4: + - .NET 10 and AOT support. + - Removed `ProcessEx` process wrapper classes, use [CliWrap](https://github.com/Tyrrrz/CliWrap) instead. + - Code cleanup with help from Copilot. +- v3.3: + - Language tags moved to a dedicated [repo](https://github.com/ptr727/LanguageTags). +- v3.2 and earlier: + - Utility classes for downloads, file and directory operations with retry logic, string compression, byte-size formatting, console helpers, and command-line parsing. diff --git a/README.md b/README.md index fae204a..6c9fa74 100644 --- a/README.md +++ b/README.md @@ -13,14 +13,16 @@ Code and Pipeline is on [GitHub](https://github.com/ptr727/Utilities)\ Packages published on [NuGet](https://www.nuget.org/packages/InsaneGenius.Utilities/)\ ![NuGet](https://img.shields.io/nuget/v/InsaneGenius.Utilities?logo=nuget) -## Version History - -- v3.4: - - .NET 10 and AOT support. - - Removed `ProcessEx` process wrapper classes, use [CliWrap](https://github.com/Tyrrrz/CliWrap) instead. - - Code cleanup with help from Copilot. -- v3.3: - - Language tags moved to a dedicated [repo](https://github.com/ptr727/LanguageTags). +## Release Notes + +**Version: 3.5**: + +**Summary**: + +- Re-synced the repository structure and agent documentation with the upstream ProjectTemplate. +- Swapped the recommended Todo VS Code add-on from Todo Tree to Better Todo Tree. + +See [Release History](./HISTORY.md) for complete release notes and older versions. ## License diff --git a/Utilities.code-workspace b/Utilities.code-workspace index b58f2dd..b78e5a8 100644 --- a/Utilities.code-workspace +++ b/Utilities.code-workspace @@ -10,7 +10,7 @@ "davidanson", "dotnettools", "extlang", - "gruntfuggly", + "fanaticpythoner", "Jernej", "macrolanguage", "nbgv", @@ -47,11 +47,11 @@ "extensions": { "recommendations": [ "davidanson.vscode-markdownlint", - "gruntfuggly.todo-tree", - "ms-dotnettools.csdevkit", + "ms-dotnettools.csdevkit", "streetsidesoftware.code-spell-checker", "editorconfig.editorconfig", - "csharpier.csharpier-vscode" + "csharpier.csharpier-vscode", + "fanaticpythoner.better-todo-tree" ] } } From e9c67e610a48a5ecf0857896ccbf1ddb09e4d6d8 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 22 Jun 2026 09:49:44 -0700 Subject: [PATCH 2/7] Re-sync CODESTYLE consolidation, task governance, and smoke gating Adopt the template changes landed since the initial re-sync (ProjectTemplate develop f526b55, 402e862), per the downstream heads-up on issue #339: - Consolidate CODESTYLE.md to the new single-guide structure (General section plus a droppable .NET section; Python dropped) and add it plus .vscode/tasks.json to the verbatim-carry contract in AGENTS.md, copilot-instructions.md, and README. - Re-scope the root .editorconfig brownfield CA batch into the owning projects' local .editorconfig at the narrowest scope, each with its own justification (the original #175 trigger): library API/async/random rules in Utilities/.editorconfig, xUnit test-code rules in UtilitiesTests/.editorconfig. - Govern .vscode/tasks.json: carry the .NET clean-compile trio verbatim (.NET Build, CSharpier Format, .NET Format with dependsOrder sequence), rename .Net Tool Update to .NET Tool Update, group convenience tasks. - Skip the release-asset zip/upload on PR smoke builds: add a smoke input to build-nugetlibrary-task.yml and forward it from build-release-task.yml. Co-Authored-By: Claude Opus 4.8 (1M context) --- .editorconfig | 42 +---- .github/copilot-instructions.md | 6 +- .github/workflows/build-nugetlibrary-task.yml | 9 + .github/workflows/build-release-task.yml | 2 + .vscode/tasks.json | 56 +++--- AGENTS.md | 12 +- CODESTYLE.md | 160 ++++++++++-------- Utilities/.editorconfig | 20 +++ UtilitiesTests/.editorconfig | 22 +++ 9 files changed, 192 insertions(+), 137 deletions(-) diff --git a/.editorconfig b/.editorconfig index 8e32cdd..2538f17 100644 --- a/.editorconfig +++ b/.editorconfig @@ -56,46 +56,12 @@ end_of_line = crlf # Default to suggestion severity dotnet_analyzer_diagnostic.severity = suggestion -# AnalysisMode=All (Directory.Build.props) enables every analyzer rule as a -# build warning, overriding the bulk suggestion default above on a per-rule -# basis; combined with TreatWarningsAsErrors that breaks the build on existing -# brownfield code. Relax the specific rules below back to suggestion — each is -# a deliberate, documented exception rather than a latent defect: -# CA1002 Public APIs intentionally expose List (e.g. FileEx.EnumerateDirectory, -# StringHistory.StringList); changing to Collection would break the -# published InsaneGenius.Utilities surface. -# CA1024 Download.GetUri is intentionally a method, not a property. -# CA1034 Unavoidable nested types generated by C# 14 `extension` members in -# Extensions.cs (the file already suppresses the related CA1708). -# CA1054 Download URL parameters are intentionally `string`, not `System.Uri`. -# CA1063 Existing IDisposable implementations are intentionally simple. -# CA1307 String operations rely on the default comparison; existing behavior -# is intentional and unchanged. -# CA1515 Library/console/test public types are intentionally public. -# CA1823 xUnit fixture fields are injected for lifetime/collection wiring and -# are not always referenced directly. -# CA1849 A few synchronous calls inside async paths are kept intentionally. -# CA2000 Stream/disposable ownership is frequently transferred (returned or -# stored), so scope-based disposal analysis reports false positives. -# CA2007 Library `await using` / `await foreach` disposal sites; the awaited -# async calls already use ConfigureAwait(false), and rewriting the -# using-declarations into ConfigureAwait blocks hurts readability. -# CA5394 Random is used for retry jitter / temp-name generation, not security. -# (IL3058 — Serilog not AOT-annotated — is a compiler/linker-level warning with +# Per-rule analyzer exceptions live in the owning project's `.editorconfig` +# (Utilities/, UtilitiesTests/) at the narrowest scope that fits, each with its +# own justification - see CODESTYLE.md "Analyzer Diagnostics and Suppressions". +# (IL3058 - Serilog not AOT-annotated - is a compiler/linker-level warning with # no source location, so it can't be set here; it's handled via NoWarn in the # AOT project files instead.) -dotnet_diagnostic.CA1002.severity = suggestion -dotnet_diagnostic.CA1024.severity = suggestion -dotnet_diagnostic.CA1034.severity = suggestion -dotnet_diagnostic.CA1054.severity = suggestion -dotnet_diagnostic.CA1063.severity = suggestion -dotnet_diagnostic.CA1307.severity = suggestion -dotnet_diagnostic.CA1515.severity = suggestion -dotnet_diagnostic.CA1823.severity = suggestion -dotnet_diagnostic.CA1849.severity = suggestion -dotnet_diagnostic.CA2000.severity = suggestion -dotnet_diagnostic.CA2007.severity = suggestion -dotnet_diagnostic.CA5394.severity = suggestion csharp_indent_block_contents = true csharp_indent_braces = false diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1ac3a75..da1fc8a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -4,7 +4,7 @@ Repository conventions for GitHub Copilot (and any other AI agent reading this f The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract this file's runbook implements. This file is intentionally narrow: commit/PR-title conventions (summarized inline so VS Code's commit-message and PR-title generators have them) plus the GitHub Copilot Review Runbook. -For language-specific style rules, see [`CODESTYLE.md`](../CODESTYLE.md) at the repo root. +For code-style rules, see [`CODESTYLE.md`](../CODESTYLE.md) at the repo root - one guide with a General section plus a .NET language section. Do not duplicate language-specific rules here. **Project-specific conventions and API/behavioral contracts also belong in [AGENTS.md](../AGENTS.md), not here** - this file is intentionally limited to the inline commit/PR-title summary and the GitHub Copilot Review Runbook. Non-Copilot agents (Claude Code, Codex, Cursor, ...) are not directed to this file and don't read it by default, so any rule a reviewer must honor has to live in `AGENTS.md` to be provider-independent. @@ -142,13 +142,13 @@ Issue-level Copilot comments (those in `issues//comments`) have no resolution Reply-body conventions: - Accepted bug/style fix: include fixing commit SHA and a one-line summary. -- Declined style comment: cite the rule (AGENTS.md or CODESTYLE.md) and the existing-tree precedent. +- Declined style comment: cite the rule (AGENTS.md or the CODESTYLE.md language section) and the existing-tree precedent. - Declined architecture proposal: one-sentence rationale. After the final push, sweep-resolve stale older threads for removed code paths. ## When in Doubt -Read [AGENTS.md](../AGENTS.md) for this repo's conventions. For language-specific rules, [`CODESTYLE.md`](../CODESTYLE.md) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. +Read [AGENTS.md](../AGENTS.md) for this repo's conventions. For code-style rules, [`CODESTYLE.md`](../CODESTYLE.md) (its General section plus the .NET section) is authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions - keep those focused on the change itself. **In a derived repo:** if you find a discrepancy that should be fixed in the template itself (this file or AGENTS.md is out of date, a rule is missing, something bit this repo and would bite the next), open an issue upstream in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) rather than only fixing it locally - see the template's [AGENTS.md "Staying in Sync and Reporting Drift Upstream"](https://github.com/ptr727/ProjectTemplate/blob/main/AGENTS.md#staying-in-sync-and-reporting-drift-upstream). diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml index 9a77094..5a8a9dd 100644 --- a/.github/workflows/build-nugetlibrary-task.yml +++ b/.github/workflows/build-nugetlibrary-task.yml @@ -23,6 +23,13 @@ on: branch: required: true type: string + # Smoke mode: build for validation only and skip the artifact zip/upload. A PR + # smoke run has no consumer for the artifact (the github-release job is gated + # `!smoke`), so uploading it just burns artifact storage. + smoke: + required: false + type: boolean + default: false outputs: # Output of the uploaded artifact id artifact-id: @@ -79,6 +86,7 @@ jobs: --skip-duplicate - name: Zip output step + if: ${{ !inputs.smoke }} run: | set -euo pipefail 7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/* @@ -86,6 +94,7 @@ jobs: # Branch-suffixed so the publisher's branch matrix can build both # branches in one run without colliding on the artifact name. - name: Upload build artifacts step + if: ${{ !inputs.smoke }} id: artifact-upload-step uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index d5ad95a..c665517 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -65,6 +65,8 @@ jobs: branch: ${{ inputs.branch }} # Conditional push to NuGet.org — never on a smoke build. push: ${{ inputs.nuget && !inputs.smoke }} + # Skip the release-asset upload on smoke (nothing consumes it on a PR). + smoke: ${{ inputs.smoke }} github-release: name: Publish GitHub release job diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8a267a1..f82fbfa 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,26 +12,39 @@ { "version": "2.0.0", "tasks": [ + // The first three tasks are the .NET clean-compile set (CODESTYLE.md) + // carried verbatim; the rest are convenience/project-specific. { "label": ".NET Build", - "type": "dotnet", - "task": "build", + "type": "process", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}", + "--verbosity=diagnostic" + ], "group": "build", - "problemMatcher": ["$msCompile"], + "problemMatcher": [ + "$msCompile" + ], "presentation": { "showReuseMessage": false, "clear": false } }, { - "label": ".NET Publish", + "label": "CSharpier Format", "type": "process", "command": "dotnet", "args": [ - "publish", - "${workspaceFolder}/Utilities/Utilities.csproj" + "csharpier", + "format", + "--log-level=debug", + "." + ], + "problemMatcher": [ + "$msCompile" ], - "problemMatcher": ["$msCompile"], "presentation": { "showReuseMessage": false, "clear": false @@ -43,6 +56,7 @@ "command": "dotnet", "args": [ "format", + "style", "--verify-no-changes", "--severity=info", "--verbosity=detailed" @@ -53,17 +67,22 @@ "presentation": { "showReuseMessage": false, "clear": false - } + }, + "dependsOrder": "sequence", + "dependsOn": [ + "CSharpier Format", + ".NET Build" + ] }, + // Convenience / project-specific tasks (adapt or drop per repo). { - "label": "CSharpier Format", + "label": ".NET Tool Update", "type": "process", "command": "dotnet", "args": [ - "csharpier", - "format", - "--log-level=debug", - "." + "tool", + "update", + "--all" ], "problemMatcher": [ "$msCompile" @@ -74,13 +93,12 @@ } }, { - "label": ".Net Tool Update", + "label": ".NET Publish", "type": "process", "command": "dotnet", "args": [ - "tool", - "update", - "--all" + "publish", + "${workspaceFolder}/Utilities/Utilities.csproj" ], "problemMatcher": [ "$msCompile" @@ -105,6 +123,6 @@ "showReuseMessage": false, "clear": false } - }, - ] + } + ] } diff --git a/AGENTS.md b/AGENTS.md index 12d3848..7c42873 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Instructions for AI Coding Agents -**Utilities** is a C# .NET NuGet library (published as `InsaneGenius.Utilities`). The library ships under [`Utilities/`](./Utilities/), with a `Sandbox/` console app for experimentation and `UtilitiesTests/` for xUnit tests. This file is the cross-cutting source of truth for process rules and this repo's project-specific conventions and public-API contracts; the C# style guidance lives in [`CODESTYLE.md`](./CODESTYLE.md). +**Utilities** is a C# .NET NuGet library (published as `InsaneGenius.Utilities`). The library ships under [`Utilities/`](./Utilities/), with a `Sandbox/` console app for experimentation and `UtilitiesTests/` for xUnit tests. This file is the cross-cutting source of truth for process rules and this repo's project-specific conventions and public-API contracts; the code-style rules live in [`CODESTYLE.md`](./CODESTYLE.md) at the repo root - one guide with a General section that applies repo-wide plus a droppable .NET language section. This repo tracks the [ptr727/ProjectTemplate](https://github.com/ptr727/ProjectTemplate) two-phase release model. It is a **NuGet-only** derivation: it has no Docker, executable, PyPI, or codegen targets, so the template's `build-docker-task.yml`, `build-executable-task.yml`, `build-pypilibrary-task.yml`, and `run-codegen-*.yml` workflows are intentionally absent, and the merge-bot carries only the Dependabot path. Keep the remaining workflow filenames and structure aligned with the template so upstream changes apply as minimal deltas. @@ -181,10 +181,12 @@ When pulling a public image fails on a Docker-Desktop/WSL credential-helper erro - `Utilities/` - the reusable .NET NuGet library (published as `InsaneGenius.Utilities`) - `Sandbox/` - console app for experimentation - `UtilitiesTests/` - xUnit tests - - **Style guide: [`CODESTYLE.md`](./CODESTYLE.md)**. + - **Style guide: [`CODESTYLE.md`](./CODESTYLE.md) ".NET" section**. - **Cross-cutting**: - `.github/` - workflows, Dependabot, Copilot instructions - - `.vscode/` - debug configs and tasks + - `.vscode/` - debug configs and tasks; the `.NET` clean-compile task group is carried verbatim (see [`CODESTYLE.md`](./CODESTYLE.md)) + +After editing code, the `.NET` clean-compile (the `.NET Format` task) must pass before commit, and brownfield status never licenses relaxing analyzer severities or silencing newly surfaced diagnostics - both rules live in [`CODESTYLE.md`](./CODESTYLE.md) "General". ## Library API Conventions @@ -205,10 +207,12 @@ These artifacts are the template's cross-cutting contract; this repo carries eac - **[`.github/copilot-instructions.md`](./.github/copilot-instructions.md)** - the whole file is a drop-in; its "GitHub Copilot Review Runbook" carries the provider mechanics. Only the `` / `` placeholders are adapted (to `ptr727` / `Utilities`). Keep it **narrow** - provider mechanics plus the inline commit/PR-title summary; project-specific conventions and API contracts belong in this file instead. - **[`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc)** - the shared lint config read by both the davidanson `markdownlint` IDE extension and CLI `markdownlint-cli2`, so the IDE and command line stay in lock-step. Carried verbatim (it is repo-agnostic). - **[`.editorconfig`](./.editorconfig) and [`.gitattributes`](./.gitattributes)** - line-ending governance. The defaults + per-extension EOL block is always-verbatim; the `[*.cs]` + ReSharper style block at the end is .NET-only (the file marks the boundary). +- **[`CODESTYLE.md`](./CODESTYLE.md)** - the single code-style guide. Its **General** section is always carried; each language section is droppable (this repo keeps the .NET section and drops the template's Python section). **Repo-root placement is load-bearing** - `AGENTS.md` links it as `./CODESTYLE.md` and `.github/copilot-instructions.md` as `../CODESTYLE.md`, so moving it breaks those links. Adapt the in-section repo-specific bits: the .NET project-folder list, the `InternalsVisibleTo` project names, and the VS Code task labels. +- **[`.vscode/tasks.json`](./.vscode/tasks.json)** - carry the **named clean-compile definitions verbatim**: the `.NET Build`, `CSharpier Format`, and `.NET Format` tasks. Their names are owned by the `CODESTYLE.md` ".NET" section and their command sequence + arguments are the canonical clean-compile spec. Convenience tasks (`.NET Tool Update`, `.NET Publish`, `Husky.Net Run`) are the adapt zone. ## Staying in Sync and Reporting Drift Upstream -This repo re-syncs against [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) periodically, not just at creation: pull the current version of each verbatim-carry artifact above and re-apply it (adapting only the noted placeholders). For [`CODESTYLE.md`](./CODESTYLE.md), re-sync from the template's .NET aggregate and adapt it to this repo's projects. +This repo re-syncs against [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate) periodically, not just at creation: pull the current version of each verbatim-carry artifact above and re-apply it (adapting only the noted placeholders). For [`CODESTYLE.md`](./CODESTYLE.md), re-sync the whole file from the template and then drop the language section(s) this repo doesn't ship (always keeping the General section) - replacing the file wholesale and trimming whole sections is simpler to keep current than hand-editing snippets. **Drift flows back upstream as an issue, not a private fix.** When re-syncing, if you find a discrepancy that should be fixed in the **template itself** - a gap, an outdated instruction, a missing rule, something that bit this repo and would bite the next derived repo too - **open an issue in [`ptr727/ProjectTemplate`](https://github.com/ptr727/ProjectTemplate)** describing it, rather than only patching it locally. A local fix realigns *this* repo; an upstream issue (then fix) corrects it for every future derived repo and keeps the template the single source of truth. This upstream-issue rule is this repo's sole cross-repo obligation: do not name sibling or downstream repos in this repo's docs, comments, or AGENTS - a reader here cares only about this project. diff --git a/CODESTYLE.md b/CODESTYLE.md index 7324c76..a22b877 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -1,78 +1,98 @@ -# Code Style and Formatting Rules - .NET +# Code Style and Formatting Rules -This file is the style guide for the .NET projects in this repo: [`Utilities/`](./Utilities/) (the published library), [`Sandbox/`](./Sandbox/) (a console app for experimentation), and [`UtilitiesTests/`](./UtilitiesTests/) (xUnit tests). +This is the single code-style guide for the repo. The **General** section applies repo-wide and is always carried. The **.NET** section is the language section for this repo's C# projects, self-contained like the `.editorconfig` `[*.cs]` block. (The upstream [ProjectTemplate](https://github.com/ptr727/ProjectTemplate) also ships a Python section; this NuGet-only repo drops it.) -Cross-cutting rules (PR titles, branching, US English, markdown style, workflow YAML, PR review etiquette) live in [AGENTS.md](./AGENTS.md) and apply across the repo. This file only documents what's specific to C# / .NET. +Cross-cutting *process* rules (PR titles, branching, US English, markdown style, comments philosophy, workflow YAML, PR review etiquette) live in [AGENTS.md](./AGENTS.md) and are not repeated here. -## Build Requirements +## General -### Zero Warnings Policy +These rules apply to every language in the repo. + +### Tooling Names and Casing + +Use each tool's official casing in task labels, docs, and prose - `.NET` (not `.Net`), `CSharpier`. Don't invent personal variants. + +### Clean-Compile Verification + +Each language defines a **clean-compile** verification - the combination of build, formatter, linter, and code-analysis tools that must report clean before a commit. It is exposed as one or more **named** VS Code tasks (or, where a language ships no tasks, documented commands), and those definitions are **carried verbatim** across derived repos. The concrete names live in the language section below. + +- **Run it after every code change.** The clean-compile must pass before you commit; CI runs the same checks as a backstop, and this repo's Husky.Net pre-commit hook runs them locally too. +- **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly; either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). + +### Analyzer Diagnostics and Suppressions + +- **A new port is not a license to silence diagnostics.** Brownfield / just-ported status never justifies relaxing analyzer or linter severities or muting newly surfaced warnings - fix them. (The only brownfield allowance in this template is the one-time git-signing / line-ending migration described in [AGENTS.md](./AGENTS.md) and [README.md](./README.md), which has nothing to do with code analysis.) +- **Suppress only genuine false-positives or deliberate, documented exceptions**, always at the **narrowest scope that fits**, in this order of preference: + 1. An **in-code annotation on the specific symbol**, with a justification - the language's attribute/comment form, never a blanket pragma spanning a region. + 2. The **owning project's local config** when the exception is project-wide for one project (e.g. a test project's own `.editorconfig`). + 3. The **root / shared config** only when the suppression is genuinely applicable to **every** project in the repo. +- **Never blanket-relax a batch of rules project-wide** to get a port to build. The per-language mechanics (which attribute, which config key) are in the language section. + +### Markdown and Spelling + +These apply repo-wide, in every directory: + +1. **Markdown linting**: All `.md` files must be lint-clean (error and warning free) via the VS Code `markdownlint` extension. [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc) at the repo root is the single source of truth - the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules it deliberately disables (e.g. `MD013` line-length, `MD033` inline HTML) are **intentional** - do not "fix" them. This file is carried verbatim by every derived repo (see [AGENTS.md "Files and Sections Derived Repos Must Carry Verbatim"](./AGENTS.md#files-and-sections-derived-repos-must-carry-verbatim)). Fix violations at the source rather than disabling rules. +2. **Spelling**: All spelling must be clean via the CSpell VS Code integration; words must be correctly spelled in **US English** (the repo-wide convention - see [AGENTS.md](./AGENTS.md)). Project-specific terms go in the workspace CSpell config. + +## .NET + +This is the style guide for the .NET projects in this repo: [`Utilities/`](./Utilities/) (the published library), [`Sandbox/`](./Sandbox/) (a console app for experimentation), and [`UtilitiesTests/`](./UtilitiesTests/) (xUnit tests). + +### Build Requirements + +#### Zero Warnings Policy **CRITICAL**: All builds must complete without warnings. The project enforces this through: -1. **VS Code tasks** - - `CSharpier Format` -> `.NET Build` -> `.NET Format` - - `.NET Format` must pass with `--verify-no-changes` before commit - - Command: `dotnet format --verify-no-changes --severity=info --verbosity=detailed` +1. **The `.NET Format` clean-compile task** (see [Clean-Compile Verification](#clean-compile-verification)) + - The .NET clean-compile is the **`.NET Format`** VS Code task, which chains `CSharpier Format` -> `.NET Build` -> `dotnet format style --verify-no-changes`. These three task definitions are carried verbatim in [`.vscode/tasks.json`](./.vscode/tasks.json). + - After any code change it must pass before commit. Run the `.NET Format` task. To run it natively instead, reproduce that task chain from [`.vscode/tasks.json`](./.vscode/tasks.json) exactly - `CSharpier Format`, then `.NET Build`, then the `dotnet format style --verify-no-changes --severity=info ...` verify - without dropping or loosening any argument (tasks.json is the canonical command spec). Bare `dotnet format` alone, skipping CSharpier or the build, is not sufficient. -2. **Analyzer configuration** +2. **Analyzer configuration** (in [`Directory.Build.props`](./Directory.Build.props)) - `latest-all` - `All` - - `true` - - Because this is a pre-existing (brownfield) library, a specific set of rules are relaxed back to suggestion in [`.editorconfig`](./.editorconfig) (each relaxation documented inline); prefer fixing new violations over adding new relaxations. + - `true` + - `true` - all warnings must be addressed, not relaxed; see [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions). 3. **Pre-commit hooks** - Husky.Net pre-commit hooks are wired in this repo (`.husky/` ships) and run CSharpier and `dotnet format` on every commit; commits are rejected if formatting fails. -### Build Tasks +#### Build Tasks -Available VS Code tasks (use via `run_task` tool): +Available VS Code tasks (run them from VS Code's task runner - **Terminal -> Run Task** - or an agent's task-running tool). The first three are the clean-compile set, carried verbatim; the rest are convenience/project-specific tasks: -- `.NET Build`: Build the solution -- `.NET Publish`: Publish the library (exercises AOT) -- `.NET Format`: Verify formatting and style (must pass) -- `CSharpier Format`: Auto-format code with CSharpier -- `.Net Tool Update`: Update dotnet tools -- `Husky.Net Run`: Run the pre-commit hook tasks on demand +- `.NET Build`: Build with diagnostic verbosity *(clean-compile)* +- `CSharpier Format`: Auto-format code with CSharpier *(clean-compile)* +- `.NET Format`: Run CSharpier and build, then verify formatting and style with `--verify-no-changes` *(clean-compile; the task to run after edits)* +- `.NET Tool Update`: Update dotnet tools *(convenience)* +- `.NET Publish`: Publish the library, exercising AOT *(project-specific)* +- `Husky.Net Run`: Run the pre-commit hook tasks on demand *(convenience)* -## Tooling and Editor +### Tooling and Editor -### Code Formatting and Tooling +#### Code Formatting and Tooling 1. **CSharpier**: Primary code formatter - - Run before committing: `dotnet csharpier format --log-level=debug .` - + - Invoked by the `CSharpier Format` task / `dotnet csharpier format --log-level=debug .` 2. **dotnet format**: Style verification - - Verify no changes: `dotnet format --verify-no-changes --severity=info --verbosity=detailed` - + - Verify no changes: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed` 3. **Other tools** - Husky.Net: pre-commit git-hook runner (installed as a dotnet tool) - - Nerdbank.GitVersioning: version management + - Nerdbank.GitVersioning: Version management -The required order is **CSharpier first, then dotnet format**. The Husky.Net pre-commit hook runs `dotnet husky run`, which applies the same checks; restore the tools with `dotnet tool restore` before the first commit. +Restore the tools with `dotnet tool restore` before the first commit. -### Editor Baseline +#### Editor Baseline 1. **Required VS Code extensions**: CSharpier, EditorConfig, markdownlint, CSpell 2. **VS Code settings**: Use the workspace settings without overrides -### Markdown Files - -1. **Linting**: All `.md` files must be linted with the VS Code `markdownlint` extension (local only; no CI) -2. **Zero warnings**: Markdown linting must be error and warning free -3. **Authoritative config**: [`.markdownlint-cli2.jsonc`](./.markdownlint-cli2.jsonc) at the repo root is the single source of truth - the davidanson `markdownlint` extension and a command-line `markdownlint-cli2` run both read it, so the IDE and CLI stay in lock-step. Rules the config deliberately disables (e.g. `MD013` line-length, `MD033` inline HTML) are **intentional** - do not "fix" them. - -### Spelling - -1. **CSpell**: All spelling checks must be error free using the CSpell VS Code integration -2. **Accepted spellings**: Words must be correctly spelled in US or UK English -3. **Allowed exceptions**: Project-specific terms must be added to the workspace CSpell config - -## Coding Standards and Conventions +### Coding Standards and Conventions Note: Code snippets are illustrative examples only. Replace namespaces/types to match the project. -### C# Language Features +#### C# Language Features 1. **File-scoped namespaces** @@ -107,7 +127,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to var name = "test"; ``` -### Naming Conventions +#### Naming Conventions 1. **Private fields**: underscore prefix with camelCase @@ -128,7 +148,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to private const int MaxRetries = 3; ``` -### Code Structure +#### Code Structure 1. **Global usings**: Use `GlobalUsings.cs` for common namespaces @@ -166,7 +186,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to 6. **`#region`**: Do not use regions. Prefer logical file/folder/namespace organization. 7. **Member ordering (StyleCop SA1201)**: const -> static readonly -> static fields -> instance readonly fields -> instance fields -> constructors -> public (events -> properties -> indexers -> methods -> operators) -> non-public in same order -> nested types -### Comments and Documentation +#### Comments and Documentation 1. **XML documentation** - `true` @@ -192,20 +212,25 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to public async Task DoWorkAsync(CancellationToken cancellationToken) {} ``` -2. **Code analysis suppressions** - - Do not use `#pragma` sections to disable analyzers - - For one-off cases, use suppression attributes with justifications - - For project-wide suppressions, add rules to `.editorconfig` +#### Analyzer Suppressions (.NET) - ```csharp - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Design", - "CA1034:Nested types should not be visible", - Justification = "https://github.com/dotnet/sdk/issues/51681" - )] - ``` +Follow the scope hierarchy in [Analyzer Diagnostics and Suppressions](#analyzer-diagnostics-and-suppressions). .NET mechanics, narrowest first: + +- **Never use `#pragma warning disable`** to silence an analyzer. +- **Symbol-scoped**: a `[System.Diagnostics.CodeAnalysis.SuppressMessage(...)]` attribute with a `Justification`, on the specific member or type: + + ```csharp + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "https://github.com/dotnet/sdk/issues/51681" + )] + ``` -### Error Handling and Logging +- **Project-scoped** (e.g. the test project): a `dotnet_diagnostic..severity` entry in *that project's own* `.editorconfig`, with a comment explaining why. This repo's library exceptions live in [`Utilities/.editorconfig`](./Utilities/.editorconfig) and its test exceptions in [`UtilitiesTests/.editorconfig`](./UtilitiesTests/.editorconfig). +- **Repo-wide**: a `dotnet_diagnostic..severity` entry in the root `.editorconfig`, only when the rule is genuinely not applicable to any project. Relaxing a batch of `CA*` rules (or `dotnet_analyzer_diagnostic.severity`) to push a brownfield port through the build is exactly what this forbids. + +#### Error Handling and Logging 1. **Serilog logging**: Use structured logging @@ -237,18 +262,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to 5. **Exceptions**: Do not swallow exceptions; log and rethrow or translate to a domain-specific exception - ```csharp - try - { - // Operation - } - catch (IOException e) when (LogOptions.Logger.LogAndHandle(e)) - { - // Retry or return false - } - ``` - -### Code Patterns +#### Code Patterns 1. **Guard clauses**: Prefer early returns for validation; use `ArgumentNullException.ThrowIfNull()` for null checks 2. **Async all the way**: Avoid blocking calls (`.Result`, `.Wait()`, `.GetAwaiter().GetResult()`); use `async`/`await` @@ -264,7 +278,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to 11. **Thread safety**: Use `Lazy` for static thread-safe instantiation; use `Lock` (C# 13+) instead of `object` for locks; avoid static mutable state and document thread-safety guarantees 12. **Performance**: Use `Span`/`Memory` for performance-critical I/O; use `ValueTask` for async methods that may complete synchronously; use `StringBuilder` for string concatenation in loops -### Testing Conventions +#### Testing Conventions 1. **Framework**: xUnit @@ -288,7 +302,7 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to 4. **Theory tests**: Use `[Theory]` with `[InlineData]` 5. **Async coverage**: Add tests for every new async method -## Project Configuration +### Project Configuration 1. **Target framework**: .NET 10.0 (`net10.0`) @@ -310,6 +324,6 @@ Note: Code snippets are illustrative examples only. Replace namespaces/types to ``` -## Best Practices +### Best Practices 1. **Code reviews**: All changes go through pull requests diff --git a/Utilities/.editorconfig b/Utilities/.editorconfig index ba0d0e5..fa6ae09 100644 --- a/Utilities/.editorconfig +++ b/Utilities/.editorconfig @@ -5,3 +5,23 @@ root = false # Allow Ex dotnet_diagnostic.CA1711.severity = none + +# Library-scoped analyzer exceptions. Each is a deliberate, documented decision +# for this published library, not a brownfield blanket-relax (see CODESTYLE.md +# "Analyzer Diagnostics and Suppressions"). +# CA1002: the published InsaneGenius.Utilities surface intentionally exposes +# List (FileEx.EnumerateDirectory, StringHistory.StringList); changing to +# Collection is a breaking API change. +dotnet_diagnostic.CA1002.severity = suggestion +# CA1024: Download.GetUri is intentionally a method, not a property. +dotnet_diagnostic.CA1024.severity = suggestion +# CA1034: nested types generated by the C# extension members in Extensions.cs. +dotnet_diagnostic.CA1034.severity = suggestion +# CA1054: Download URL parameters are intentionally string, not System.Uri. +dotnet_diagnostic.CA1054.severity = suggestion +# CA2007: await using / await foreach disposal sites; the awaited async calls +# already use ConfigureAwait(false) and a ConfiguredAsyncDisposable rewrite +# hurts readability. +dotnet_diagnostic.CA2007.severity = suggestion +# CA5394: Random is used for retry jitter and temp-name generation, not security. +dotnet_diagnostic.CA5394.severity = suggestion diff --git a/UtilitiesTests/.editorconfig b/UtilitiesTests/.editorconfig index 9acafa5..4601a6c 100644 --- a/UtilitiesTests/.editorconfig +++ b/UtilitiesTests/.editorconfig @@ -8,3 +8,25 @@ dotnet_diagnostic.CA1707.severity = none # Ignore unused private members dotnet_diagnostic.IDE0052.severity = none + +# Test-scoped analyzer exceptions: rules that target production-code concerns +# and don't apply to xUnit test code. Each is documented, not a brownfield +# blanket-relax (see CODESTYLE.md "Analyzer Diagnostics and Suppressions"). +# CA1063: test IDisposable implementations are intentionally simple. +dotnet_diagnostic.CA1063.severity = suggestion +# CA1307: test string operations use the default comparison intentionally. +dotnet_diagnostic.CA1307.severity = suggestion +# CA1515: xUnit requires public test classes, so they can't be made internal. +dotnet_diagnostic.CA1515.severity = suggestion +# CA1823: xUnit fixture fields are injected for lifetime/collection wiring and +# are not always referenced directly. +dotnet_diagnostic.CA1823.severity = suggestion +# CA1849: synchronous calls inside async test paths are kept intentionally. +dotnet_diagnostic.CA1849.severity = suggestion +# CA2000: test disposable ownership is transferred or scoped to the test, so +# scope-based disposal analysis reports false positives. +dotnet_diagnostic.CA2000.severity = suggestion +# CA2007: ConfigureAwait(false) is not used in xUnit tests (see xUnit1030). +dotnet_diagnostic.CA2007.severity = suggestion +# CA5394: Random in tests is for test data, not security. +dotnet_diagnostic.CA5394.severity = suggestion From 624238f695dc95a2fa47012071020be6381489b0 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 22 Jun 2026 10:00:15 -0700 Subject: [PATCH 3/7] Address review: drop sibling-repo link, normalize workspace indentation - HISTORY.md: describe the v3.3 language-tags split generically instead of naming/linking the sibling repo, per the no-sibling-repos rule in AGENTS.md. - Utilities.code-workspace: normalize the recommendations array to consistent tab indentation. Co-Authored-By: Claude Opus 4.8 (1M context) --- HISTORY.md | 2 +- Utilities.code-workspace | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 25f8d69..97ec9ba 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -13,6 +13,6 @@ Some useful and not so useful C# .NET utility classes. - Removed `ProcessEx` process wrapper classes, use [CliWrap](https://github.com/Tyrrrz/CliWrap) instead. - Code cleanup with help from Copilot. - v3.3: - - Language tags moved to a dedicated [repo](https://github.com/ptr727/LanguageTags). + - Language tags split out into a separate dedicated library. - v3.2 and earlier: - Utility classes for downloads, file and directory operations with retry logic, string compression, byte-size formatting, console helpers, and command-line parsing. diff --git a/Utilities.code-workspace b/Utilities.code-workspace index b78e5a8..e04fdd0 100644 --- a/Utilities.code-workspace +++ b/Utilities.code-workspace @@ -46,10 +46,10 @@ }, "extensions": { "recommendations": [ - "davidanson.vscode-markdownlint", + "davidanson.vscode-markdownlint", "ms-dotnettools.csdevkit", - "streetsidesoftware.code-spell-checker", - "editorconfig.editorconfig", + "streetsidesoftware.code-spell-checker", + "editorconfig.editorconfig", "csharpier.csharpier-vscode", "fanaticpythoner.better-todo-tree" ] From f4f1ef846bcd18155151c48113b5564d512b7f48 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 22 Jun 2026 10:07:25 -0700 Subject: [PATCH 4/7] Address review: current-state README notes, ASCII workflow comments - README.md: phrase the v3.5 Release Notes summary in current-state terms; the before/after change framing stays in HISTORY.md (the changelog). - build-release-task.yml: replace em dashes in comments with ASCII hyphens. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/build-release-task.yml | 6 +++--- README.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index c665517..ea77f83 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -63,7 +63,7 @@ jobs: # commit than the one the release tag (also GitCommitId) points at. ref: ${{ needs.get-version.outputs.GitCommitId }} branch: ${{ inputs.branch }} - # Conditional push to NuGet.org — never on a smoke build. + # Conditional push to NuGet.org - never on a smoke build. push: ${{ inputs.nuget && !inputs.smoke }} # Skip the release-asset upload on smoke (nothing consumes it on a PR). smoke: ${{ inputs.smoke }} @@ -97,7 +97,7 @@ jobs: # NBGV can produce a SemVer2 that was already released. GitHub release # creation has no built-in skip-duplicate (unlike NuGet's # `--skip-duplicate`), and re-publishing an unchanged version is exactly - # the churn the two-phase model avoids — so skip the release step when a + # the churn the two-phase model avoids - so skip the release step when a # release for this tag already exists. - name: Check for existing release step id: release-exists @@ -120,7 +120,7 @@ jobs: # `target_commitish` MUST be set explicitly: softprops doesn't pass a # default through, and GitHub's REST API then defaults the new tag to # the repository's default branch (main). We pin it to NBGV's - # `GitCommitId` — the exact commit the version was computed from. This + # `GitCommitId` - the exact commit the version was computed from. This # avoids two bugs: `github.sha` would be wrong (the publisher's branch # matrix builds `develop` from a run whose `github.sha` is main's tip), # and `inputs.branch` would be a moving ref (a commit landing mid-run diff --git a/README.md b/README.md index 6c9fa74..5ea1cbc 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Packages published on [NuGet](https://www.nuget.org/packages/InsaneGenius.Utilit **Summary**: -- Re-synced the repository structure and agent documentation with the upstream ProjectTemplate. -- Swapped the recommended Todo VS Code add-on from Todo Tree to Better Todo Tree. +- Repository structure and agent documentation follow the upstream ProjectTemplate. +- Better Todo Tree is the recommended Todo VS Code add-on. See [Release History](./HISTORY.md) for complete release notes and older versions. From 2be9ce30d266b990bbff929079238b1b8f779ede Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 22 Jun 2026 10:12:45 -0700 Subject: [PATCH 5/7] Reindent workspace file to 4-space per .editorconfig Normalize Utilities.code-workspace to 4-space indentation throughout so it matches the repo-wide `.editorconfig` (`indent_style = space`, `indent_size = 4`) instead of the prior mixed tabs/spaces. Co-Authored-By: Claude Opus 4.8 (1M context) --- Utilities.code-workspace | 76 ++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/Utilities.code-workspace b/Utilities.code-workspace index e04fdd0..7e3908a 100644 --- a/Utilities.code-workspace +++ b/Utilities.code-workspace @@ -1,31 +1,31 @@ { - "folders": [ - { - "path": "." - } - ], - "settings": { - "cSpell.words": [ - "csdevkit", - "davidanson", - "dotnettools", - "extlang", - "fanaticpythoner", - "Jernej", - "macrolanguage", - "nbgv", - "Nerdbank", - "nupkg", - "Serilog", - "Simoncic", - "softprops", - "stefanzweifel", - "subtag", - "templating", - "triggerbuild", - "winget" - ], - "dotnet.defaultSolution": "Utilities.sln", + "folders": [ + { + "path": "." + } + ], + "settings": { + "cSpell.words": [ + "csdevkit", + "davidanson", + "dotnettools", + "extlang", + "fanaticpythoner", + "Jernej", + "macrolanguage", + "nbgv", + "Nerdbank", + "nupkg", + "Serilog", + "Simoncic", + "softprops", + "stefanzweifel", + "subtag", + "templating", + "triggerbuild", + "winget" + ], + "dotnet.defaultSolution": "Utilities.sln", "files.trimTrailingWhitespace": true, "[markdown]": { "files.trimTrailingWhitespace": false @@ -43,15 +43,15 @@ "csharp.debug.symbolOptions.searchNuGetOrgSymbolServer": true, "csharp.debug.symbolOptions.searchMicrosoftSymbolServer": true, "files.encoding": "utf8" - }, - "extensions": { - "recommendations": [ - "davidanson.vscode-markdownlint", - "ms-dotnettools.csdevkit", - "streetsidesoftware.code-spell-checker", - "editorconfig.editorconfig", - "csharpier.csharpier-vscode", - "fanaticpythoner.better-todo-tree" - ] - } + }, + "extensions": { + "recommendations": [ + "davidanson.vscode-markdownlint", + "ms-dotnettools.csdevkit", + "streetsidesoftware.code-spell-checker", + "editorconfig.editorconfig", + "csharpier.csharpier-vscode", + "fanaticpythoner.better-todo-tree" + ] + } } From 96208c7a8f4480df94c0bc4cd94a8869aebee8f0 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 22 Jun 2026 10:18:09 -0700 Subject: [PATCH 6/7] Fix workspace default solution to Utilities.slnx dotnet.defaultSolution pointed at Utilities.sln, but the repo's solution file is Utilities.slnx (no .sln exists), so VS Code/.NET tooling could not open the intended solution by default. Co-Authored-By: Claude Opus 4.8 (1M context) --- Utilities.code-workspace | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utilities.code-workspace b/Utilities.code-workspace index 7e3908a..d3a6110 100644 --- a/Utilities.code-workspace +++ b/Utilities.code-workspace @@ -25,7 +25,7 @@ "triggerbuild", "winget" ], - "dotnet.defaultSolution": "Utilities.sln", + "dotnet.defaultSolution": "Utilities.slnx", "files.trimTrailingWhitespace": true, "[markdown]": { "files.trimTrailingWhitespace": false From b4192fd1f251bd65105303b19e4e4d621b58257d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 22 Jun 2026 10:25:13 -0700 Subject: [PATCH 7/7] Correct CA1024 suppression justification to the real method The CA1024 comment in Utilities/.editorconfig named Download.GetUri, which does not exist; the rule triggers on Download.GetHttpClient(). Reference the actual method so the suppression rationale is accurate. Co-Authored-By: Claude Opus 4.8 (1M context) --- Utilities/.editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utilities/.editorconfig b/Utilities/.editorconfig index fa6ae09..0ab9e9a 100644 --- a/Utilities/.editorconfig +++ b/Utilities/.editorconfig @@ -13,7 +13,7 @@ dotnet_diagnostic.CA1711.severity = none # List (FileEx.EnumerateDirectory, StringHistory.StringList); changing to # Collection is a breaking API change. dotnet_diagnostic.CA1002.severity = suggestion -# CA1024: Download.GetUri is intentionally a method, not a property. +# CA1024: Download.GetHttpClient() is intentionally a method, not a property. dotnet_diagnostic.CA1024.severity = suggestion # CA1034: nested types generated by the C# extension members in Extensions.cs. dotnet_diagnostic.CA1034.severity = suggestion