Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 140 additions & 92 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
root = true

# -------------------------------
# General
# -------------------------------
# ============================================================
# Canonical .editorconfig for the NextIteration estate.
#
# Copied verbatim into every governed repo (STANDARD.md §5.2).
# Posture: a DELIBERATE ALLOW-LIST of style gates — there is no
# blanket `dotnet_analyzer_diagnostic.severity`, so a style rule a
# future SDK ships never auto-gates the build. Every rule that IS
# gated below (`:warning`) is a hard build failure under
# `TreatWarningsAsErrors`; that is intentional — the build is what
# forces the code into the ordained style. Code-quality (CA) rules
# keep their `AnalysisLevel=latest` defaults.
# ============================================================

# ---------- All files ----------
[*]
charset = utf-8
end_of_line = lf
Expand All @@ -11,99 +22,136 @@ indent_style = space
indent_size = 4
trim_trailing_whitespace = true

# -------------------------------
# C# files
# -------------------------------
[*.cs]

indent_size = 4

# New lines & braces
csharp_new_line_before_open_brace = all
csharp_prefer_braces = true:warning

# Using directives
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = true

# var usage (Spectre-style: pragmatic)
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = false:suggestion

# Expression-bodied members (used where clean)
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = false:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = when_on_single_line:suggestion

# Pattern matching / modern C#
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion

# Nullability helpers
dotnet_style_null_propagation = true:suggestion
dotnet_style_coalesce_expression = true:suggestion

# Readonly fields
dotnet_style_readonly_field = true:suggestion

# -------------------------------
# Naming
# -------------------------------
[*.{csproj,props,targets}]
indent_size = 2

# Private fields: _camelCase
dotnet_naming_rule.private_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields
dotnet_naming_rule.private_fields_should_be_camel_case.style = camel_case_with_underscore
[*.{json,yml,yaml}]
indent_size = 2

dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
# A const IS a field, so without this the rule demands `_nonceSize` for
# `private const int NonceSize` — PascalCase constants are correct .NET style and
# the codebase uses them throughout. Restricting the rule to instance fields keeps
# it aimed at what it was written for. Found when EnforceCodeStyleInBuild surfaced
# 76 IDE1006 violations, every one of them a constant.
dotnet_naming_symbols.private_fields.required_modifiers =

dotnet_naming_style.camel_case_with_underscore.capitalization = camel_case
dotnet_naming_style.camel_case_with_underscore.required_prefix = _

# Interfaces: IMyInterface
dotnet_naming_rule.interfaces_should_start_with_i.severity = suggestion
dotnet_naming_rule.interfaces_should_start_with_i.symbols = interfaces
dotnet_naming_rule.interfaces_should_start_with_i.style = interface_prefix
# Trailing whitespace is a hard line break in Markdown
[*.md]
trim_trailing_whitespace = false

dotnet_naming_symbols.interfaces.applicable_kinds = interface
# ---------- C# ----------
[*.cs]
indent_size = 4

# Braces & new lines
csharp_new_line_before_open_brace = all
csharp_prefer_braces = true:warning # braces always (IDE0011)

# Namespaces — block-scoped estate-wide (IDE0160)
csharp_style_namespace_declarations = block_scoped:warning

# using directives
csharp_using_directive_placement = outside_namespace:warning # IDE0065
dotnet_sort_system_directives_first = true # feeds IDE0055 (gated below)
dotnet_separate_import_directive_groups = true # feeds IDE0055 — matches estate style

# 'this.' qualification — never used in this estate
dotnet_style_qualification_for_field = false:warning # IDE0003
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning

# Accessibility — always explicit
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning # IDE0040

# var — ordained: var everywhere it is legal (IDE0007)
csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = true:warning

# Expression-bodied members — single-line only; block-bodied constructors (IDE0021-0027)
csharp_style_expression_bodied_methods = when_on_single_line:warning
csharp_style_expression_bodied_constructors = false:warning
csharp_style_expression_bodied_operators = when_on_single_line:warning
csharp_style_expression_bodied_properties = when_on_single_line:warning

# Pattern matching / null handling (IDE0019/0020/0029/0030/0031)
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_pattern_matching_over_as_with_null_check = true:warning
dotnet_style_null_propagation = true:warning
dotnet_style_coalesce_expression = true:warning

# readonly fields (IDE0044) — only flags never-reassigned fields, so genuinely mutable state is safe
dotnet_style_readonly_field = true:warning

# Modern syntax — gated toward the modern form
dotnet_style_prefer_collection_expression = when_types_loosely_match:warning # IDE0300+
csharp_style_implicit_object_creation_when_type_is_apparent = true:warning # IDE0090

# Primary constructors — NOT forced. IDE0290 is one-directional (it can only push toward primary
# constructors), and forcing them onto service classes with real initialisation is a downgrade.
# Advisory only; existing class primary constructors are left as they are.
csharp_style_prefer_primary_constructors = false:suggestion # IDE0290

# Unused expression/assignment values — never nudge toward `_ =` discards (IDE0058/IDE0059).
# House rule: no discard solely to swallow a return value (see CLAUDE.md for the carve-outs).
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
csharp_style_unused_value_assignment_preference = discard_variable:silent

# ---------- Naming (gated) ----------
# Matches estate reality: _camelCase private fields (incl. static readonly), PascalCase const
# fields, PascalCase types/members, I-prefixed interfaces, T-prefixed type parameters, camelCase
# locals/parameters. `const_fields` is declared before `private_fields`: a const is a field, so
# both specs match it and precedence decides — const → PascalCase must win.

# Styles
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.camel_case_underscore.required_prefix = _
dotnet_naming_style.camel_case_underscore.capitalization = camel_case
dotnet_naming_style.camel_case_plain.capitalization = camel_case
dotnet_naming_style.interface_prefix.required_prefix = I
dotnet_naming_style.interface_prefix.capitalization = pascal_case
dotnet_naming_style.type_param_prefix.required_prefix = T
dotnet_naming_style.type_param_prefix.capitalization = pascal_case

# -------------------------------
# Analyzers
# -------------------------------

# Keep warnings visible but not painful
dotnet_analyzer_diagnostic.severity = warning

# Unused usings
dotnet_diagnostic.IDE0005.severity = warning

# Simplification
dotnet_diagnostic.IDE0007.severity = suggestion
dotnet_diagnostic.IDE0008.severity = suggestion

# Documentation (Spectre.Console is pragmatic here)
dotnet_diagnostic.CS1591.severity = silent

# -------------------------------
# JSON / YAML
# -------------------------------
[*.json]
indent_size = 2

[*.yml]
indent_size = 2

[*.yaml]
indent_size = 2
# Symbols
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter
dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate
dotnet_naming_symbols.non_field_members.applicable_kinds = property, method, event
dotnet_naming_symbols.const_fields.applicable_kinds = field
dotnet_naming_symbols.const_fields.required_modifiers = const
dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, private_protected, internal, protected_internal
dotnet_naming_symbols.locals_and_params.applicable_kinds = parameter, local

# Rules (all warning = gated)
dotnet_naming_rule.interfaces_i.severity = warning
dotnet_naming_rule.interfaces_i.symbols = interfaces
dotnet_naming_rule.interfaces_i.style = interface_prefix

dotnet_naming_rule.type_params_t.severity = warning
dotnet_naming_rule.type_params_t.symbols = type_parameters
dotnet_naming_rule.type_params_t.style = type_param_prefix

dotnet_naming_rule.types_pascal.severity = warning
dotnet_naming_rule.types_pascal.symbols = types
dotnet_naming_rule.types_pascal.style = pascal_case

dotnet_naming_rule.members_pascal.severity = warning
dotnet_naming_rule.members_pascal.symbols = non_field_members
dotnet_naming_rule.members_pascal.style = pascal_case

dotnet_naming_rule.const_pascal.severity = warning
dotnet_naming_rule.const_pascal.symbols = const_fields
dotnet_naming_rule.const_pascal.style = pascal_case

dotnet_naming_rule.private_underscore.severity = warning
dotnet_naming_rule.private_underscore.symbols = private_fields
dotnet_naming_rule.private_underscore.style = camel_case_underscore

dotnet_naming_rule.locals_camel.severity = warning
dotnet_naming_rule.locals_camel.symbols = locals_and_params
dotnet_naming_rule.locals_camel.style = camel_case_plain

# ---------- Option-less / compiler severities ----------
dotnet_diagnostic.IDE0005.severity = warning # unnecessary usings (needs GenerateDocumentationFile — set by §1.6)
dotnet_diagnostic.IDE0055.severity = warning # formatting: whitespace, using sort/groups
dotnet_diagnostic.CS1591.severity = warning # public members MUST carry XML docs (STANDARD §1.6, CLAUDE.md non-negotiable)

# ---------- Deliberate exemptions (advisory, never gate) ----------
dotnet_diagnostic.IDE0046.severity = suggestion # convert to conditional expression — nested ternaries hurt readability
dotnet_diagnostic.IDE0058.severity = suggestion # unused expression value — see the discard house rule above
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Adopted the revised canonical `.editorconfig` and enabled `EnforceCodeStyleInBuild`** (NextIteration.Standards §5.2, §1.2.1 — the latter now a `MUST`). The canonical file is a deliberate allow-list of gated style rules rather than a blanket `dotnet_analyzer_diagnostic.severity`, so a style rule a future SDK ships never auto-gates the build. With the flag on, the gated rules fail the build under `TreatWarningsAsErrors` instead of merely showing in the IDE. Bringing the code green was a mechanical, behaviour-preserving reformat of 92 sites — braces on all single-statement `if`s (IDE0011, 64 of them), collection expressions (IDE0300/IDE0301/IDE0028), `var` usage, two expression-bodied members, one simplified null check, and five unnecessary usings — applied with `dotnet format` plus the collection-expression sites it cannot fix automatically. All 392 tests (196 × `net8.0`/`net10.0`) pass unchanged, and the build stays at zero warnings.
- **`IDE0005` suppressed in the non-shipping projects.** It only runs in-build when `GenerateDocumentationFile` is `true`, which both the test and demo projects set to `false` — sample and fixture code carries no XML docs, and `TreatWarningsAsErrors` would fail the build over every missing one. With `EnforceCodeStyleInBuild` on it hard-errors demanding the doc file be enabled instead. NextIteration.Standards §2.7 mandates this for the test project; the demo is the same shape (not shipped, so not public surface) and needs the same opt-out. `IDE0005` still gates the shipping project, where the doc file is on — and it caught five real unnecessary usings there.

- **`release.yml` folded into `ci.yml`.** Publishing now happens in the same workflow run as the build, so the `publish` job pushes the artifact this run's `build` job produced — the exact bytes the gate tested. The old tag-triggered `release.yml` rebuilt from the tag and published an artifact no gate had ever seen. It also globbed `*.nupkg` when uploading, so the `.snupkg` was built and then silently never published; the glob is now `*nupkg` and symbols ship. Repointing the nuget.org Trusted Publishing policy from `release.yml` to `ci.yml` was part of the same change, because the policy is bound to a workflow filename.
- **CI now has a single aggregating gate job, `ci`, and it is the only required status check.** `build` and `test` were required directly before, which couples the branch ruleset to the matrix: `test`'s check names carry the matrix values, so adding or dropping a platform broke protection. The gate declares `needs: [build, test]` with `if: always()` and fails on any upstream result that is not success — including `skipped`, which branch protection would otherwise read as satisfied.
- **Every workflow declares `concurrency`, explicit `permissions`, and per-job `timeout-minutes`.** Superseded pushes cancel instead of stacking up, except on tags — a half-cancelled release can leave an incomplete package set on nuget.org. NuGet restore is cached on `~/.nuget/packages`.
Expand Down
20 changes: 18 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,15 +1,30 @@
<Project>

<!--
STANDARD.md 1.2: every property that is identical across projects lives here
rather than being restated per csproj. Fifteen copies is fifteen chances to
diverge, and that duplication is how these repos drifted apart. A csproj
carries only what is genuinely specific to it.
diverge, and the divergence is silent — nothing fails when one csproj quietly
disagrees with its sibling about DebugType.

A csproj carries only what is genuinely specific to it: PackageId, Version,
Description, PackageTags, TargetFrameworks, the package's own URLs, and any
real exception. A non-shipping project (tests, a demo) sets
GenerateDocumentationFile=false to opt back out — sample and fixture code has
no XML docs, and TreatWarningsAsErrors would otherwise fail the build over
every missing one.

EnforceCodeStyleInBuild is on (STANDARD.md 1.2.1). It runs the IDE analyzers
in-build, so the canonical .editorconfig (5.2) actually gates the house style
rather than merely documenting it. The blanket severity that once turned every
advisory preference into an error (490 in Auth) is gone; .editorconfig is now a
deliberate allow-list of named gates. See 1.2.1 for the per-rule decisions.
-->
<PropertyGroup>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<EnablePackageValidation>true</EnablePackageValidation>
<IncludeSymbols>true</IncludeSymbols>
Expand All @@ -24,4 +39,5 @@
<Authors>Stuart Meeks</Authors>
<Company>Next Iteration</Company>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
the test project (STANDARD.md 2.7).
-->
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<!--
IDE0005 (remove unnecessary usings) only runs in-build when
GenerateDocumentationFile is true, which the line above sets to
false. With EnforceCodeStyleInBuild on (STANDARD.md 1.2.1) it
hard-errors demanding the doc file be enabled instead. STANDARD.md
2.7 resolves this for test projects; the demo is the same shape —
not shipped, so not public surface — and needs the same opt-out.
IDE0005 still gates the shipping project, where the doc file is on.
-->
<NoWarn>$(NoWarn);IDE0005</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using NextIteration.SpectreConsole.SelfUpdate.Commands;

using Spectre.Console.Cli;

namespace NextIteration.SpectreConsole.SelfUpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,10 @@ private Task<UpdateConflictResolution> PromptForConflictAsync(UpdateConflict con
return Task.FromResult(keep ? UpdateConflictResolution.KeepExisting : UpdateConflictResolution.UseNew);
}

private static bool IsUpdateAvailable(string current, string latestTag)
{
// Defer to the same comparator the checker uses so behaviour is
// identical between the cached probe and this fresh one.
return Pipeline.UpdateChecker.IsNewer(current, latestTag);
}
// Defer to the same comparator the checker uses so behaviour is
// identical between the cached probe and this fresh one.
private static bool IsUpdateAvailable(string current, string latestTag) =>
Pipeline.UpdateChecker.IsNewer(current, latestTag);

private static string StageLabel(UpdateStage stage) => stage switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,9 @@ public Task InstallAsync(

public async Task InstallAsync(IProgress<UpdateProgressEvent>? progress = null, CancellationToken ct = default)
{
var release = await _source.GetLatestAsync(_options.Channel, ct).ConfigureAwait(false);
if (release is null)
{
throw new UpdateException(
var release = await _source.GetLatestAsync(_options.Channel, ct).ConfigureAwait(false)
?? throw new UpdateException(
"No release is available from the configured update source. The source either returned null or is currently unreachable.");
}
await _installer.InstallAsync(release, progress, onConflict: null, ct).ConfigureAwait(false);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ internal static class UpdateCacheFile
{
try
{
if (!File.Exists(path)) return null;
if (!File.Exists(path))
{
return null;
}

var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<UpdateCacheEntry>(json, JsonOpts);
}
Expand Down
Loading