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
16 changes: 16 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,26 @@ jobs:
# Analyse source only. obj/ and bin/ hold generated and compiled
# output — e.g. the xUnit auto-generated entry point — so findings
# there are noise against code no human maintains.
#
# query-filters excludes the two audit queries that fire on every
# P/Invoke declaration and call site (cs/unmanaged-code,
# cs/call-to-unmanaged-code). Native-backend packages (Keychain,
# libsecret, DPAPI) exist to call unmanaged code, so these are pure
# noise there and non-native repos have no P/Invoke for them to hit.
# This excludes ONLY those two queries — every other
# security-and-quality query still runs on the interop files, so no
# real finding is lost (STANDARD.md 4.4). This repo has no P/Invoke,
# so the block matches nothing here; it is carried to keep the
# workflow identical to the template (STANDARD.md 3.0.1).
config: |
paths-ignore:
- "**/obj/**"
- "**/bin/**"
query-filters:
- exclude:
id: cs/unmanaged-code
- exclude:
id: cs/call-to-unmanaged-code

# Explicit build rather than autobuild: these repos multi-target, and
# autobuild has picked a single TFM in the past, silently analysing half
Expand Down
39 changes: 34 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Test-infrastructure and repository maintenance. The library's public API and
target frameworks are unchanged; the only consumer-visible change is a servicing
bump to the `net10.0` `Microsoft.Extensions.DependencyInjection.Abstractions`
floor.
_Nothing yet._

## [1.0.0] — 2026-08-21

First stable release. This is a milestone, not a rewrite: the public API and the
shipped target frameworks (`net8.0` and `net10.0`) are unchanged from 0.3.0. What
1.0.0 adds is a commitment — from here the public surface follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) strictly, so a breaking
change to it requires a 2.0.0. The work in this cycle is the test-infrastructure,
repository maintenance and standards alignment below; the only consumer-visible
runtime change is a servicing bump to the `net10.0`
`Microsoft.Extensions.DependencyInjection.Abstractions` floor.

### Changed

Expand Down Expand Up @@ -83,6 +91,26 @@ floor.
- Adopted the canonical `.gitignore` and `.editorconfig`. The `.editorconfig` change
scopes the private-field naming rule to instance fields — a `const` is a field, so
the rule previously demanded `_nonceSize` for `private const int NonceSize`.
- Enabled `EnforceCodeStyleInBuild` and adopted the revised canonical
`.editorconfig` (`STANDARD.md` 1.2.1 and 5.2). The IDE style analyzers now run
in-build under `TreatWarningsAsErrors`, so the ordained house style — braces
always, block-scoped namespaces, `var` throughout, explicit accessibility,
collection expressions, the naming ruleset, and `CS1591` public-API docs — is
gated by the build rather than merely documented. The `.editorconfig` is now a
deliberate allow-list of named gates rather than a blanket severity, so a style
rule a future SDK ships never auto-fails the build. The clause had been blocked
on exactly that blanket-severity problem. Bringing the code to green under the
flag converted two collection initialisations in `SettingsStore` to collection
expressions and the test project's file-scoped namespaces to block-scoped — no
runtime behaviour changed, and all 64 tests (32 × `net8.0`/`net10.0`) still pass.
The test project also adds `IDE0005` to its `NoWarn` (`STANDARD.md` 2.7): that
rule only runs with `GenerateDocumentationFile` on, which the test project turns
off, so gating it would otherwise force the doc file back on.
- Added a CodeQL `query-filters` block excluding `cs/unmanaged-code` and
`cs/call-to-unmanaged-code` (`STANDARD.md` 4.4). This library has no P/Invoke, so
the filter matches nothing here; it is carried to keep `codeql.yml` aligned with
the canonical template (`STANDARD.md` 3.0.1), which non-native repos share
harmlessly.
- Moved the build properties shared by both projects out of the individual csprojs
and into the root `Directory.Build.props`. Both projects previously restated the
same fifteen properties, which is fifteen chances for one copy to drift silently.
Expand Down Expand Up @@ -209,7 +237,8 @@ floor.
- `TreatWarningsAsErrors=true`, `AnalysisLevel=latest` — zero-warning public API.
- Package icon, with the editable source vector kept under `design/icons/`.

[Unreleased]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.Settings/compare/v0.3.0...HEAD
[Unreleased]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.Settings/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.Settings/compare/v0.3.0...v1.0.0
[0.3.0]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.Settings/releases/tag/v0.3.0
[0.2.0]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.Settings/releases/tag/v0.2.0
[0.1.1]: https://github.com/StuartMeeks/NextIteration.SpectreConsole.Settings/releases/tag/v0.1.1
Expand Down
11 changes: 7 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@
GenerateDocumentationFile, because sample and fixture code has no XML docs
and TreatWarningsAsErrors would fail the build over it).

EnforceCodeStyleInBuild is deliberately NOT here. STANDARD.md 1.2.1 is
blocked: TreatWarningsAsErrors promotes every advisory .editorconfig
preference to a hard failure, which produced 490 build errors in a sibling
repo. It needs a per-rule gate-versus-advisory decision first.
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 clause was blocked
while the blanket severity turned every advisory preference into a hard
failure (490 in a sibling repo); that blanket is gone and the .editorconfig
is now a deliberate allow-list of named gates, so the flag is enforceable.
-->
<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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<PropertyGroup>
<PackageId>NextIteration.SpectreConsole.Settings</PackageId>
<Version>0.3.0</Version>
<Version>1.0.0</Version>
<Description>Strongly-typed, JSON-persisted settings for CLI tools, with automatic or explicit persistence and ready-made Spectre.Console settings commands.</Description>
<GeneratePackageOnBuild Condition="'$(Configuration)' == 'Release'">true</GeneratePackageOnBuild>
<PackageOutputPath>$(MSBuildThisFileDirectory)..\..\artifacts\packages</PackageOutputPath>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ private static async Task ReplaceAtomicallyAsync(string tempPath, string path)
}
}

// Unique per call so concurrent writers don't collide on a shared
// "{path}.tmp" name.
private static string BuildTempPath(string finalPath) =>
// Unique per call so concurrent writers don't collide on a shared
// "{path}.tmp" name.
$"{finalPath}.{Guid.NewGuid():N}.tmp";

private static void TryDelete(string path)
Expand Down
Loading