From e945d99e606f6096e8ffed175a40e445576109d7 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 13:59:01 +0300 Subject: [PATCH 01/21] docs(03): capture phase context --- .../03-CONTEXT.md | 139 ++++++++++++++++++ .../03-DISCUSSION-LOG.md | 74 ++++++++++ 2 files changed, 213 insertions(+) create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-DISCUSSION-LOG.md diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md new file mode 100644 index 0000000..4654db1 --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md @@ -0,0 +1,139 @@ +# Phase 3: Public Surface, Packaging & Binder Cleanup - Context + +**Gathered:** 2026-07-14 +**Status:** Ready for planning + + +## Phase Boundary + +Trim the public API and packages to only meaningful, correctly-scoped surface, and fix the +command-line binder to parse real-world arguments — the last breaking changes batched before the +first `v2.0.0-beta`. Requirements **API-01, PKG-01, PKG-02, SRC-02** are locked by ROADMAP; this +discussion only settles HOW. + +**Not in scope:** AOT-01 (reflection/AOT-trim annotations) and DOC-01 (README refresh) — Phase 4; +REL-01 (cut the beta) — Phase 5; COLL-01 (`List` support, still owner-deferred); held D1/D2. + + + +## Implementation Decisions + +### API-01 — `SettingsHolder` → internal (breaking) +- **D-01:** Flip `public class SettingsHolder` (`SettingsHolder.cs:5`) to **`internal sealed class`**. + `ISettingsHolder` is already `internal` (`ISettingsHolder.cs:5`); the sole consumer is + `SettingsCollection` (`SettingsCollection.cs:10,14`), which is internal. Seal it — not designed for + inheritance. Breaking, but free in the pre-stable window. Tests keep access via `InternalsVisibleTo` + (`Info.cs:3-4` → UnitTests + Benchmark). + +### PKG-01 — `Core.AspNet` → drop the package (breaking) +- **D-02:** **Remove the `Core.AspNet` package entirely.** Its only type is + `internal static class Environments` (3 const strings) → it ships **zero public surface** and merely + duplicates ASP.NET Core's own `Environments`/`EnvironmentName` constants. Removal touches three places: + the project directory, its entry in `SimpleSettings.slnx:13`, and the `ProjectReference` in + `UnitTests.csproj:19`. Confirm no test actually consumes `Environments` (remove any such use). +- **D-02b:** The project is packable by SDK default (no explicit `IsPackable`). If a `Core.AspNet` + prerelease alpha was ever pushed to NuGet, it simply stops publishing on removal; note it for + deprecation/unlisting if it exists (pre-stable alphas → low stakes; planner/researcher to confirm). + +### PKG-02 — `Microsoft.Extensions.*` floor → float per-TFM (breaking-ish for consumers' floor) +- **D-03:** Float the floor **per-TFM via CPM conditional `PackageVersion`** in + `src/Directory.Packages.props` (`:8-12`): **net8 → latest `8.0.x` patch**, **net10 → `10.0.x`** + (currently `10.0.9`). Applies to all four packages: `Microsoft.Extensions.Configuration`, + `.Configuration.Json`, `.DependencyInjection`, `.DependencyInjection.Abstractions`. This frees a net8 + consumer from being transitively forced onto 10.x. Researcher/planner resolves the exact current + latest `8.0.x`. Verify build + full suite green on **both** TFMs afterward. + +### SRC-02 — command-line binder (behavior change) +- **D-04:** Add **space-separated `--key value`** support (lookahead pairing) *in addition to* the + existing inline `--key=value` / `--key:value`. Rule: a prefixed key followed by a **non-prefixed** + token consumes that token as its value; a following token that itself starts with a prefix (`-`/`/`) + is a **new key**, not a value (the current key then has no value → not stored, preserving today's + string-only "no value ⇒ skip" semantics). This is the real "quoted value with spaces" fix — such a + value arrives shell-unquoted as its own token, which the current parser drops. +- **D-05:** **Skip `arg[0]` (exe path) by default**, exposed as a toggle on + `CommandLineSettingsBinderOptions` (e.g. `SkipFirstArgument`, default `true`) so callers passing + `Main(string[])` args — which already exclude the exe — can turn it off. +- **D-06:** Preserve existing options (prefixes `-`/`/`, delimiters `:`/`=`, `IsCaseSensitive`, + `NameFormatter`). No manual quote-stripping needed (the shell already unquotes). + +### Claude's Discretion +- Exact CPM conditional syntax; the precise latest `8.0.x` version; the option name (`SkipFirstArgument` + or similar); new binder test placement (`Tests/.../Binders/`); whether `arg[0]` skip is unconditional + (default) vs exe-path-heuristic. All breaking changes are expected and batched pre-beta. + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Requirement / roadmap +- `.planning/ROADMAP.md` — Phase 3 goal + 4 success criteria +- `.planning/REQUIREMENTS.md` — API-01 / PKG-01 / PKG-02 / SRC-02 + traceability +- `FIX-PLAN.md` (frozen historical) — §A5/A3/A4/A6 per-item detail; trust current source over any stale wording + +### API-01 (SettingsHolder → internal) +- `src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs:5` — `public class` to flip to `internal sealed` +- `src/Core/ExistForAll.SimpleSettings/ISettingsHolder.cs:5` — already `internal` +- `src/Core/ExistForAll.SimpleSettings/SettingsCollection.cs:10,14` — sole consumer +- `src/Core/ExistForAll.SimpleSettings/Info.cs:3-4` — `InternalsVisibleTo` (UnitTests, Benchmark) + +### PKG-01 (drop Core.AspNet) +- `src/Core/ExistForAll.SimpleSettings.Core.AspNet/Environments.cs` — only file (`internal static Environments`) +- `src/Core/ExistForAll.SimpleSettings.Core.AspNet/ExistForAll.SimpleSettings.Core.AspNet.csproj` — project to remove +- `src/SimpleSettings.slnx:13` — solution entry to remove +- `src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj:19` — `ProjectReference` to remove + +### PKG-02 (per-TFM floor) +- `src/Directory.Packages.props:8-12` — the four `Microsoft.Extensions.*` pins @ `10.0.9` + +### SRC-02 (command-line binder) +- `src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs` — `Parse` / `SplitByDelimiter` (add lookahead + arg[0] skip) +- `src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs` — options (add `SkipFirstArgument`) +- `.planning/codebase/TESTING.md` — TUnit conventions for the new binder tests + +### Release / CI context +- `.github/workflows/ci.yml` (net8 + net10), `release.yml` (alpha on master push — no paths filter), `benchmark.yml` (allocation gate) + + + +## Existing Code Insights + +### Reusable Assets +- `ISectionBinder` contract — the CLI binder fix stays entirely within it (no new abstraction). +- Central Package Management (`src/Directory.Packages.props`) — conditional `PackageVersion` per TFM is the mechanism for PKG-02. +- `InternalsVisibleTo` (`Info.cs`) — keeps the internal-flipped `SettingsHolder` reachable from tests. + +### Established Patterns +- One public type per file; `I`-prefixed interfaces; block-scoped namespaces; `net8.0;net10.0` multi-target only. +- TUnit tests under `src/Tests/ExistForAll.SimpleSettings.UnitTests/` (add CLI-binder tests under `Binders/`); build + test from `src/`. +- **TUnit/Microsoft.Testing.Platform gotcha:** `dotnet test --filter "*X*"` exits 5 (zero tests); use `--treenode-filter "/*/*/ClassNameTests/*"` or run unfiltered. +- Breaking changes are free pre-stable and are batched before `v2.0.0-beta`. + +### Integration Points +- `SimpleSettings.slnx` + `UnitTests.csproj` — remove `Core.AspNet` references (PKG-01). +- `Directory.Packages.props` — per-TFM float (PKG-02). +- `CommandLineSettingsBinder.Parse` loop — lookahead + arg[0] skip (SRC-02). + + + +## Specific Ideas +- Ship Phase 3 on **this feature branch → PR** (never doc-only/direct to `master` — every master push publishes a throwaway alpha via `release.yml`). Push/PR via the `guy-lud` account (origin uses the `github-guy-lud` SSH alias; for `gh`, `gh auth switch --user guy-lud` then switch back to `guy-frontegg`). +- All four items are deliberate breaking/behavior changes, expected in this pre-beta cleanup. + + + +## Deferred Ideas +- **AOT-01** (reflection/AOT-trim annotations) and **DOC-01** (README refresh) — Phase 4. +- **REL-01** (cut the first `v2.0.0-beta`) — Phase 5. +- **COLL-01** (`List`/`IList`/`ICollection` support) — still owner-deferred (originally Phase 2). +- **D1 Validations**, **D2 EqualityCompererCreator** — HELD; out of scope. +- **CLI binder getopt-style short flags / boolean flags** — not adopted; a string-value binder doesn't model booleans. Out of scope. + +None of the above are blockers for Phase 3. + + +--- + +*Phase: 3-public-surface-packaging-binder-cleanup* +*Context gathered: 2026-07-14* diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-DISCUSSION-LOG.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-DISCUSSION-LOG.md new file mode 100644 index 0000000..16dcb25 --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-DISCUSSION-LOG.md @@ -0,0 +1,74 @@ +# Phase 3: Public Surface, Packaging & Binder Cleanup - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-07-14 +**Phase:** 3-public-surface-packaging-binder-cleanup +**Areas discussed:** Core.AspNet (PKG-01), Microsoft.Extensions floor (PKG-02), CLI binder (SRC-02), SettingsHolder (API-01) + +--- + +## Core.AspNet (PKG-01) + +| Option | Description | Selected | +|--------|-------------|----------| +| Drop the package | Remove Core.AspNet from the .slnx/solution; nothing references it publicly; ASP.NET consumers already get these constants from Microsoft.Extensions.Hosting. | ✓ | +| Make Environments public | Keep the package; flip the class to `public static` so the 3 constants become its consumable surface. | | +| Fold into GenericHost, drop pkg | Move Environments (public) into the existing GenericHost package, then remove the standalone Core.AspNet package. | | + +**User's choice:** Drop the package. +**Notes:** Core.AspNet holds one `internal static class Environments` (3 const strings) → zero public surface shipped, duplicating ASP.NET Core's own `Environments` constants. Removal touches `SimpleSettings.slnx:13` and `UnitTests.csproj:19` (ProjectReference). + +--- + +## Microsoft.Extensions floor (PKG-02) + +| Option | Description | Selected | +|--------|-------------|----------| +| Float per-TFM, 8.0.0 net8 floor | Conditional PackageVersion: net8 → 8.0.0 (lowest), net10 → 10.0.x. Widest net8 compatibility. | | +| Float per-TFM, latest 8.0.x | Same split but require a recent 8.0.x floor (picks up security patches). | ✓ | +| Keep 10.0.9 pin + document | Leave both TFMs on 10.0.9 with a justification note; net8 consumers stay forced onto 10.x. | | + +**User's choice:** Float per-TFM, latest 8.0.x floor for net8 (10.0.x for net10). +**Notes:** All four `Microsoft.Extensions.*` currently pinned `10.0.9` in `Directory.Packages.props`. Exact latest 8.0.x patch to be resolved by research/planning. + +--- + +## CLI binder (SRC-02) + +| Option | Description | Selected | +|--------|-------------|----------| +| Add --key value + skip arg[0] | Lookahead-pair a prefixed key with the next token as its value; keep inline `=`/`:`; skip arg[0] by default. | ✓ | +| Add --key value, arg[0]-skip opt-in | Same space-separated support, but arg[0]-skip off by default. | | +| Minimal: arg[0] skip only | Skip arg[0], keep inline-delimiter-only parsing; document that `--key value` isn't supported. | | + +**User's choice:** Add space-separated `--key value` + skip arg[0] by default. +**Notes:** Today the binder only parses inline `--key=value`/`--key:value`; a space-separated `--key value` (how a quoted value with spaces arrives, as its own token) is silently dropped. A following token starting with a prefix is treated as a new key, not a value. `arg[0]` skip exposed as a default-on option toggle. + +--- + +## SettingsHolder (API-01) + +| Option | Description | Selected | +|--------|-------------|----------| +| internal sealed class | Flip to `internal sealed`; tests reach it via InternalsVisibleTo. Breaking, but free pre-stable. | ✓ | +| internal (not sealed) | Flip to internal but leave inheritable. | | +| Keep public | Only if an external consumer references it directly (unlikely pre-stable). | | + +**User's choice:** `internal sealed class`. +**Notes:** `ISettingsHolder` is already internal; only `SettingsCollection` uses `SettingsHolder`. `InternalsVisibleTo` covers UnitTests + Benchmark. + +## Claude's Discretion + +- Exact CPM conditional syntax and the precise latest 8.0.x version (PKG-02). +- Option naming for the arg[0] toggle (e.g. `SkipFirstArgument`) and whether the skip is unconditional or exe-path-heuristic (SRC-02). +- New binder test placement (`Tests/.../Binders/`). + +## Deferred Ideas + +- AOT-01 (reflection/AOT-trim annotations) + DOC-01 (README) — Phase 4. +- REL-01 (cut v2.0.0-beta) — Phase 5. +- COLL-01 (`List` support) — still owner-deferred. +- D1 Validations / D2 EqualityCompererCreator — HELD. +- CLI binder getopt-style short/boolean flags — not adopted (string-value binder doesn't model booleans). From 6cbf3700d4c3e1e074a5013c3a84eec057bc8486 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 13:59:02 +0300 Subject: [PATCH 02/21] docs(state): record phase 3 context session --- .planning/STATE.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.planning/STATE.md b/.planning/STATE.md index 0c528a3..68510ee 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -5,8 +5,8 @@ milestone_name: milestone current_phase: 3 current_phase_name: Public Surface, Packaging & Binder Cleanup status: verifying -stopped_at: "Phase 1 (S1 #27, C2 #28) shipped; reconciled ROADMAP/STATE/REQUIREMENTS to mark Phase 1 complete. Next: plan Phase 2." -last_updated: "2026-07-14T10:16:29.256Z" +stopped_at: Phase 3 context gathered +last_updated: "2026-07-14T10:59:02.042Z" last_activity: 2026-07-14 last_activity_desc: Phase 02 complete, transitioned to Phase 3 progress: @@ -95,6 +95,6 @@ Items acknowledged and carried forward: ## Session Continuity -Last session: 2026-07-14T10:08:58.479Z -Stopped at: Phase 1 (S1 #27, C2 #28) shipped; reconciled ROADMAP/STATE/REQUIREMENTS to mark Phase 1 complete. Next: plan Phase 2. -Resume file: None +Last session: 2026-07-14T10:59:02.037Z +Stopped at: Phase 3 context gathered +Resume file: .planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md From d8e7a89497d7a03bafcc9d40a91e379c6120fe05 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 14:14:14 +0300 Subject: [PATCH 03/21] docs(03): research public-surface/packaging/binder cleanup phase --- .../03-RESEARCH.md | 426 ++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md new file mode 100644 index 0000000..e0e9389 --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md @@ -0,0 +1,426 @@ +# Phase 3: Public Surface, Packaging & Binder Cleanup - Research + +**Researched:** 2026-07-14 +**Domain:** .NET library public-surface hygiene, NuGet Central Package Management (per-TFM floors), command-line argument parsing +**Confidence:** HIGH + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01 (API-01):** Flip `public class SettingsHolder` (`SettingsHolder.cs:5`) to **`internal sealed class`**. `ISettingsHolder` is already `internal`; sole consumer `SettingsCollection` is internal. Tests keep access via `InternalsVisibleTo` (`Info.cs:3-4` → UnitTests + Benchmark). +- **D-02 (PKG-01):** **Remove the `Core.AspNet` package entirely.** Only type is `internal static class Environments` (3 const strings) → zero public surface, duplicates ASP.NET Core's own `Environments`. Removal touches: the project directory, `SimpleSettings.slnx:13`, and the `ProjectReference` in `UnitTests.csproj:19`. Confirm no test consumes `Environments`. +- **D-02b:** Project is packable by SDK default. If a `Core.AspNet` prerelease alpha was pushed to NuGet, it stops publishing on removal; note deprecation/unlisting if it exists (pre-stable → low stakes). +- **D-03 (PKG-02):** Float the floor **per-TFM via CPM conditional `PackageVersion`** in `src/Directory.Packages.props` (`:8-12`): **net8 → latest `8.0.x` patch**, **net10 → `10.0.x` (currently `10.0.9`)**. All four packages: `Microsoft.Extensions.Configuration`, `.Configuration.Json`, `.DependencyInjection`, `.DependencyInjection.Abstractions`. Verify build + full suite green on **both** TFMs. +- **D-04 (SRC-02):** Add **space-separated `--key value`** support (lookahead pairing) *in addition to* inline `--key=value` / `--key:value`. Rule: a prefixed key followed by a **non-prefixed** token consumes it as value; a following token starting with a prefix (`-`/`/`) is a **new key**, not a value (current key then has no value → not stored, preserving today's "no value ⇒ skip" semantics). +- **D-05 (SRC-02):** **Skip `arg[0]` (exe path) by default**, exposed as a toggle on `CommandLineSettingsBinderOptions` (e.g. `SkipFirstArgument`, default `true`) so `Main(string[])` callers (which already exclude the exe) can turn it off. +- **D-06 (SRC-02):** Preserve existing options (prefixes `-`/`/`, delimiters `:`/`=`, `IsCaseSensitive`, `NameFormatter`). No manual quote-stripping (shell already unquotes). + +### Claude's Discretion +- Exact CPM conditional syntax; the precise latest `8.0.x` version; the option name (`SkipFirstArgument` or similar); new binder test placement (`Tests/.../Binders/CommandLine/`); whether `arg[0]` skip is unconditional (default) vs exe-path-heuristic. All breaking changes are expected and batched pre-beta. + +### Deferred Ideas (OUT OF SCOPE) +- **AOT-01** (reflection/AOT-trim annotations) and **DOC-01** (README refresh) — Phase 4. +- **REL-01** (cut the first `v2.0.0-beta`) — Phase 5. +- **COLL-01** (`List`/`IList`/`ICollection`) — owner-deferred. +- **D1 Validations**, **D2 EqualityCompererCreator** — HELD. +- **CLI binder getopt-style short flags / boolean flags** — not adopted (a string-value binder doesn't model booleans). OUT OF SCOPE. + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| API-01 | Make `SettingsHolder`/`ISettingsHolder` internal (breaking) | Verified: `SettingsHolder` referenced only inside its own assembly (`SettingsCollection.cs:10,14`); no test references it; no PublicAPI analyzer baseline to update. Flip to `internal sealed` is self-contained. | +| PKG-01 | `Core.AspNet` exposes a public type or the package is dropped | Verified: package's `Environments` is `internal` with **no** `InternalsVisibleTo`; the test project's `Environments` is a **local duplicate** in a different namespace → the `ProjectReference` is **dead**. Package **is published** on NuGet (`1.0.0` + `2.0.0-alpha.0.*`) → unlisting step documented. | +| PKG-02 | Float `Microsoft.Extensions.*` floor per-TFM | Verified empirically: conditional `PackageVersion` per TFM restores net8→`8.0.x`, net10→`10.0.9`, **no NU1504**, clean build. Exact latest `8.0.x` per package resolved below. | +| SRC-02 | CLI binder parses quoted values with spaces + skips `arg[0]` | Verified: parity source (dotnet/runtime `CommandLineConfigurationProvider`) quoted; arg[0] rationale confirmed — `AddCommandLine` uses `Environment.CommandLine.Split(' ')` which **includes the exe path** as `args[0]`. | + + +## Summary + +All four items are small, well-scoped, and independently verifiable; three are pure edits with no new dependencies, and the fourth (PKG-02) is a Central Package Management mechanics question that I settled **empirically** by running a throwaway multi-target restore. There are no research unknowns blocking planning — every "HOW" is resolved with a verified answer. + +The one genuinely non-obvious finding: the **latest `8.0.x` patch differs per package** (`Configuration` = `8.0.0`, `.Configuration.Json` = `8.0.1`, `.DependencyInjection` = `8.0.1`, `.DependencyInjection.Abstractions` = `8.0.2`), so the four conditional entries carry three different net8 versions. Conditional `` duplicated per TFM is valid CPM and produces **no NU1504** because NuGet restore is per-TFM and evaluates the conditions — I confirmed this with a live restore that resolved net8→8.0.x and net10→10.0.9. A second finding for SRC-02: `Core.AspNet` **has been published** to NuGet.org as `1.0.0` and a long run of `2.0.0-alpha.0.N` prereleases, so removal should be paired with unlisting the alphas (low stakes, pre-stable). + +For the CLI binder, the decided lookahead rule (**D-04**) *deliberately diverges* from Microsoft's `CommandLineConfigurationProvider`, which consumes the next token as a value **unconditionally** (even if it starts with a prefix). SimpleSettings instead treats a prefixed next-token as a new key. This is a correct, intentional design choice — plan the tests around SimpleSettings' rule, not exact Microsoft parity. Separately, the `AddCommandLine()` convenience method's `Environment.CommandLine.Trim().Split(' ')` is the real reason `SkipFirstArgument` defaults to `true` (arg[0] is the exe path) — and it is itself quote-unsafe (see Pitfall 3). + +**Primary recommendation:** Ship all four as one PR on `chore/gsd-ownership-cutover` (never direct to `master` — every master push publishes a throwaway alpha). Use conditional `PackageVersion` items (two per package) for PKG-02; validate PKG-02 by inspecting `obj/project.assets.json` per TFM. Keep the binder fix inside `Parse`/`SplitByDelimiter` + one new option; add TUnit tests under `Binders/CommandLine/`. + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| `SettingsHolder` visibility (API-01) | Core library (`ExistForAll.SimpleSettings`) | Test assembly (via `InternalsVisibleTo`) | Internal DTO; owned entirely by the core assembly's collection type | +| `Core.AspNet` removal (PKG-01) | Solution / packaging | NuGet.org (unlist) | Build-graph + package-registry concern, not runtime code | +| Dependency floor (PKG-02) | Build / NuGet (`Directory.Packages.props`) | CI restore (both TFMs) | Central Package Management owns version policy per TFM | +| CLI argument parsing (SRC-02) | Binders extension (`ISectionBinder`) | Options type | Stays inside the existing binder contract; no new abstraction | + +## Standard Stack + +No new packages are introduced by this phase. The stack is unchanged; only **versions** move (PKG-02) and one project is **removed** (PKG-01). + +### Core (unchanged, versions adjusted by PKG-02) +| Library | net8 floor | net10 | Purpose | Why Standard | +|---------|-----------|-------|---------|--------------| +| Microsoft.Extensions.Configuration | **8.0.0** | 10.0.9 | Config abstractions consumed by `ConfigurationBinder` | Microsoft first-party (BCL-adjacent) | +| Microsoft.Extensions.Configuration.Json | **8.0.1** | 10.0.9 | JSON config source (test fixtures + consumers) | Microsoft first-party | +| Microsoft.Extensions.DependencyInjection | **8.0.1** | 10.0.9 | DI container integration (`AddSimpleSettings`) | Microsoft first-party | +| Microsoft.Extensions.DependencyInjection.Abstractions | **8.0.2** | 10.0.9 | DI contracts (`IServiceCollection`) | Microsoft first-party | + +*All four net8 patches `[VERIFIED: nuget.org flatcontainer index, 2026-07-14]` — the highest stable `8.0.x` for each package. All four net10 `[VERIFIED: nuget.org]` — `10.0.9` is the current highest `10.0.x` (matches the existing pin).* + +**Note on the version choice (Claude's Discretion per CONTEXT):** the decision says "latest `8.0.x` patch". Two coherent options: +1. **Latest-patch-per-package (recommended, matches decision):** the table above (`8.0.0 / 8.0.1 / 8.0.1 / 8.0.2`). Verified to restore coherently — NuGet unifies the mixed `8.0.x` sub-versions. +2. **Uniform `8.0.0` for all four:** simpler, lowest possible floor (friendliest to net8 consumers), enables a single shared MSBuild property. Equally valid; only choose if uniformity is preferred over "latest patch". +Either is correct; the planner should surface both and let the owner pick. The default is Option 1. + +### Testing / Build (unchanged) +| Library | Version | Purpose | +|---------|---------|---------| +| TUnit | 1.58.0 | Test framework on Microsoft.Testing.Platform | +| BenchmarkDotNet | 0.15.8 | Allocation gate (net10 only) | +| MinVer | 7.0.0 | git-tag-driven versioning (`v` prefix, `2.0` floor) | + +**Version verification:** +```bash +# net8 floors (run per package; highest stable 8.0.x): +curl -s https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration/index.json # -> 8.0.0 (only stable 8.0.x) +curl -s https://api.nuget.org/v3-flatcontainer/microsoft.extensions.configuration.json/index.json # -> 8.0.1 +curl -s https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection/index.json # -> 8.0.1 +curl -s https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencyinjection.abstractions/index.json # -> 8.0.2 +``` + +## Package Legitimacy Audit + +No external packages are **added** in this phase (PKG-02 only changes versions of already-present Microsoft first-party packages; PKG-01 **removes** a project). All four `Microsoft.Extensions.*` are first-party, billions of downloads, source at `github.com/dotnet/runtime`. + +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| +| Microsoft.Extensions.Configuration | NuGet | 7+ yrs | billions | github.com/dotnet/runtime | OK | Approved (version change only) | +| Microsoft.Extensions.Configuration.Json | NuGet | 7+ yrs | billions | github.com/dotnet/runtime | OK | Approved (version change only) | +| Microsoft.Extensions.DependencyInjection | NuGet | 7+ yrs | billions | github.com/dotnet/runtime | OK | Approved (version change only) | +| Microsoft.Extensions.DependencyInjection.Abstractions | NuGet | 7+ yrs | billions | github.com/dotnet/runtime | OK | Approved (version change only) | + +**Packages removed due to [SLOP] verdict:** none +**Packages flagged as suspicious [SUS]:** none +**New packages installed:** none — no `checkpoint:human-verify` gate required for this phase. + +## Architecture Patterns + +### System Architecture Diagram (CLI binder data flow — SRC-02) + +``` +Consumer SimpleSettings binder chain +-------- --------------------------- +Main(string[] args) ─┐ + ├─► AddArguments(args, opts) ─┐ +Environment.CommandLine (exe + args) │ + .Trim().Split(' ') ─► AddCommandLine(opts) ──────┤ + ▼ + new CommandLineSettingsBinder(args, options) + │ + Parse(args): (SRC-02 change here) + ├─ [SkipFirstArgument] skip args[0] (default true) ◄── D-05 + └─ for each arg (with lookahead): ◄── D-04 + ├─ inline "--k=v" / "--k:v" → store (existing) + ├─ "--k" + next non-prefixed → store k=next + └─ "--k" + next prefixed → k has no value → skip + │ + ▼ + _argumentStore : Dictionary + │ + BindPropertySettings(context): key = NameFormatter?(section,key) ?? context.Key + │ TryGetValue → context.SetNewValue(value) + ▼ + ValuesPopulator (last-writer-wins across binders) +``` + +### Pattern 1: Per-TFM conditional `PackageVersion` (PKG-02) — VERIFIED +**What:** Two `` items per package, each gated on `$(TargetFramework)`. +**When to use:** CPM repo that multi-targets and needs a different floor per TFM. +**Example (drop-in replacement for `Directory.Packages.props:7-13`):** +```xml + + + + + + + + + + + + +``` +**Verified result** (live restore of an equivalent `net8.0;net10.0` project): +- `net8.0` target resolved `Microsoft.Extensions.Configuration/8.0.0`, `.Configuration.Json/8.0.1`. +- `net10.0` target resolved both at `10.0.9`. +- `dotnet build /p:TreatWarningsAsErrors=true` → **0 warnings, 0 errors** (no NU1504, no NU1008). + +**Alternative pattern (property indirection)** — cleaner only if you adopt uniform `8.0.0`: +```xml + + 8.0.0 + 10.0.9 + + + + ... + +``` +This needs **one** version value shared across all four, so it only fits Option 2 (uniform `8.0.0`). Because the latest patches differ per package (Option 1), the conditional-item form above is the recommended shape. + +### Pattern 2: CLI single-token lookahead (SRC-02, D-04) +**What:** When the current token is a prefixed key with no inline delimiter, peek the next token; consume it as the value **iff** it is not itself prefixed. +**Reference (dotnet/runtime `CommandLineConfigurationProvider.Load`, `main`):** +```csharp +// Source: github.com/dotnet/runtime .../CommandLineConfigurationProvider.cs +int separator = currentArg.IndexOf('='); +if (separator < 0) { + if (keyStartIndex == 0) continue; // no prefix, no '=' -> ignored + // ... switch-mapping handling (SimpleSettings has none) ... + key = currentArg.Substring(keyStartIndex); + if (!enumerator.MoveNext()) continue; // missing value -> ignore + value = enumerator.Current; // <-- consumes NEXT token UNCONDITIONALLY +} else { + key = currentArg.Substring(keyStartIndex, separator - keyStartIndex); + value = currentArg.Substring(separator + 1); // first '=' splits; rest (incl '=') is value +} +data[key] = value; // last-writer-wins +``` +**Parity note (IMPORTANT):** Microsoft takes `enumerator.Current` as the value *without* checking whether it is prefixed. **D-04 deliberately diverges:** if the next token is prefixed (`-`/`/`), SimpleSettings treats it as a **new key** and leaves the current key valueless (→ not stored). Plan tests to SimpleSettings' rule. The `=`-splitting behavior (first delimiter wins, remainder is the value including further delimiters) **does** match SimpleSettings' existing `SplitByDelimiter` (min-index delimiter, `Substring(idx+1)`). + +### Anti-Patterns to Avoid +- **Adding switch-mappings / short-flag semantics** — explicitly OUT OF SCOPE (deferred). Do not import Microsoft's `-x`→`--long` mapping or short-switch `FormatException`. +- **Manual quote-stripping in the binder** — the shell already unquotes; `Main(string[])` tokens arrive clean (D-06). +- **A single unconditional `PackageVersion` plus a second conditional one** — that *would* trip NU1504 (two active items for one package). Both items must be mutually-exclusively conditioned. +- **Hard-deleting the published `Core.AspNet` versions** — NuGet policy forbids true deletion; **unlist** instead. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Robust exe+args tokenization for `AddCommandLine` | `Environment.CommandLine.Split(' ')` (current) | `Environment.GetCommandLineArgs()` | `.Split(' ')` shreds quoted paths/values with spaces; `GetCommandLineArgs()` returns a properly tokenized `string[]` with `[0]` = exe path (see Pitfall 3). | +| Per-TFM version policy | ad-hoc ``/`.targets` hacks | conditional `` in `Directory.Packages.props` | CPM natively supports it; verified NU1504-free. | +| Env-name constants | keeping `Core.AspNet.Environments` | ASP.NET Core's `Microsoft.Extensions.Hosting.Environments` | Framework already ships `Development`/`Staging`/`Production`. | + +**Key insight:** the whole phase is *removing* hand-rolled/duplicated surface, not adding abstractions. The only "build" is ~15 lines in `Parse`. + +## Runtime State Inventory + +> PKG-01 removes a **published** package; that is external registry state, captured here. + +| Category | Items Found | Action Required | +|----------|-------------|------------------| +| Stored data | None — no datastore keys reference `SettingsHolder`/`Core.AspNet`/`Environments`. | none | +| Live service config | **NuGet.org registry:** `ExistForAll.SimpleSettings.Core.AspNet` is published: `1.0.0` (legacy) + `2.0.0-alpha.0.110 … 2.0.0-alpha.0.N` prereleases (pushed by `release.yml` on every master push). Removing the project stops **future** publishing but existing versions remain listed. | **Unlist** the `2.0.0-alpha.0.*` prereleases (and optionally `1.0.0`) via `dotnet nuget delete -s https://api.nuget.org/v3/index.json --non-interactive` (unlists, does not delete) or the nuget.org UI. Low stakes (pre-stable alphas). Do this with the publishing account. | +| OS-registered state | None. | none | +| Secrets/env vars | None affected — no key names change. | none | +| Build artifacts | Stale `obj/`+`bin/` under `src/Core/ExistForAll.SimpleSettings.Core.AspNet/` (contains a leftover `netstandard2.0` AssemblyInfo from an earlier multi-target). Removed with the directory. | Delete the project directory; `dotnet restore` regenerates the solution graph. | + +**Canonical question — after every file is updated, what still carries the old package?** Only NuGet.org (the already-published alphas). Everything else is in-repo and removed by deleting the directory + two references. + +## Common Pitfalls + +### Pitfall 1: NU1504 duplicate `PackageVersion` (PKG-02) +**What goes wrong:** Two `PackageVersion` items for one package error out as duplicates. +**Why it happens:** NU1504 fires only when **both** items are active simultaneously (i.e., at least one is unconditional). +**How to avoid:** Ensure **every** item carries a mutually-exclusive `Condition` on `$(TargetFramework)`. Verified: conditioned duplicates restore cleanly with **0 warnings**. +**Warning signs:** `error NU1504: Duplicate 'PackageVersion' items found`. If seen, a condition is missing or non-exclusive. + +### Pitfall 2: net8 consumer still forced to 10.x (PKG-02 regression) +**What goes wrong:** After the edit, a net8 consumer still pulls `Microsoft.Extensions.* 10.x`. +**Why it happens:** The library's net8 TFM group still declares `>= 10.x` (edit didn't take, or a condition mismatched the actual TFM string). +**How to avoid:** Validate by restoring and inspecting `obj/project.assets.json` — the `net8.0` target must list `8.0.x`, not `10.x`. (`` is **not** set → default off, so only direct deps are pinned; this is correct and needs no change.) +**Warning signs:** `project.assets.json` `targets["net8.0"]` shows `Microsoft.Extensions.Configuration/10.0.9`. + +### Pitfall 3: `AddCommandLine()` shreds quoted values / the arg[0] contract (SRC-02) +**What goes wrong:** `AddCommandLine()` uses `Environment.CommandLine.Trim().Split(' ')` — this (a) splits a quoted value/path with spaces into multiple tokens, and (b) includes the **exe path** as `args[0]`. +**Why it happens:** `Environment.CommandLine` is the raw, un-tokenized string; naive `Split(' ')` ignores quoting. +**How to avoid:** +- `SkipFirstArgument` default **true** is correct for the `AddCommandLine()` path (drops the exe path). +- For the `AddArguments(Main(string[] args))` path, `Main` args **exclude** the exe, so a caller must set `SkipFirstArgument = false` or the first real arg is dropped — document this on the option. +- **Strongly consider** switching `AddCommandLine()` to `Environment.GetCommandLineArgs()` (properly tokenized, `[0]` = exe) so D-04's quoted-value fix actually helps the `AddCommandLine()` path too. This is arguably in-scope for "quoted value with spaces binds correctly" (see Open Questions). +**Warning signs:** `--name "John Doe"` binds `Name="John"` (or nothing) instead of `John Doe`. + +### Pitfall 4: existing `ArgumentsTests` under the new default (SRC-02) +**What goes wrong:** Adding `SkipFirstArgument` default-true silently drops a real first arg and breaks a test. +**Why it happens (and why it's fine here):** The existing test uses `" name=value age:3".Split(' ')` = `["", "name=value", "age:3"]` — the **leading space** makes `args[0]` an empty string. Skipping `""` is harmless; `name`/`age` are at `[1]`/`[2]`. The suite stays green. +**How to avoid:** Keep the leading-space fixture as-is, **or** rewrite it to `["name=value","age:3"]` and pass `SkipFirstArgument=false`. Either preserves intent. Add new cases for the space-separated form. +**Warning signs:** `Build_WhenNameMatchKey_ShouldPlaceValue` fails after the option lands. + +### Pitfall 5: TUnit filter (project-wide, from TESTING.md) +**What goes wrong:** `dotnet test --filter "*CommandLine*"` exits 5 (zero tests). +**How to avoid:** Use `--treenode-filter "/*/*/ArgumentsTests/*"` or run unfiltered. Runner is Microsoft.Testing.Platform, not VSTest. + +## Code Examples + +### API-01 — the exact flip (`SettingsHolder.cs:5`) +```csharp +// before +public class SettingsHolder : ISettingsHolder +// after +internal sealed class SettingsHolder : ISettingsHolder +``` +No other change needed: `ISettingsHolder` is already `internal`; `SettingsCollection` (internal) is the only consumer; `Info.cs` already grants `InternalsVisibleTo` to UnitTests + Benchmark. No PublicAPI analyzer/baseline exists in the repo (`grep` for `PublicAPI*.txt` / `Microsoft.CodeAnalysis.PublicApiAnalyzers` → none), so there is no API-surface file to update. + +### PKG-01 — the three removal points +``` +1. Delete dir: src/Core/ExistForAll.SimpleSettings.Core.AspNet/ +2. src/SimpleSettings.slnx — delete line 13: + +3. src/Tests/.../ExistForAll.SimpleSettings.UnitTests.csproj — delete line 19: + +``` +The test project's own `Core/AspNet/Environments.cs` (namespace `...UnitTests.Core.AspNet`) is a **local duplicate** and stays; it does not reference the package (the package's `Environments` is `internal` with no `InternalsVisibleTo` to the test assembly). Verified: no `using ExistForAll.SimpleSettings.Core.AspNet` anywhere in `src/`. + +### SRC-02 — new option (`CommandLineSettingsBinderOptions.cs`) +```csharp +/// Skip args[0] (the executable path) when parsing. +/// Default true — correct for Environment.CommandLine / GetCommandLineArgs(). +/// Set false when passing Main(string[] args), which already excludes the exe. +public bool SkipFirstArgument { get; set; } = true; +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Single unconditional pin (`10.0.9` all TFMs) | Per-TFM conditional `PackageVersion` | This phase | net8 consumers no longer forced to 10.x | +| `Core.AspNet.Environments` constants | ASP.NET Core's built-in `Environments` | This phase | One dead package removed | +| Inline `--k=v` only + drop bare tokens | `--k=v` **and** `--k v` lookahead | This phase | Quoted-with-spaces values bind | + +**Deprecated/outdated:** `ExistForAll.SimpleSettings.Core.AspNet` package — unlist its NuGet alphas after removal. + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | Unlisting the `Core.AspNet` alphas is desired handling for a pre-stable package (vs leaving them listed). | Runtime State Inventory | Low — cosmetic registry hygiene; owner may choose to leave them. Requires the publishing (`guy-lud`) account. | +| A2 | `Benchmark` assembly does not reference `Core.AspNet` (only UnitTests + slnx do). | PKG-01 | Low — verified by grep of `*.csproj`; if a hidden ref exists the build would fail loudly at restore. | + +**Note:** A1/A2 are the only non-VERIFIED items; both are low-risk registry/graph hygiene, not code correctness. + +## Open Questions + +1. **Should `AddCommandLine()` switch from `Environment.CommandLine.Split(' ')` to `Environment.GetCommandLineArgs()`?** + - What we know: the current split is quote-unsafe; the success criterion is "a quoted command-line value with spaces binds correctly". The D-04 lookahead fix only helps callers who pass already-tokenized args (`Main`/`GetCommandLineArgs`), **not** the `Environment.CommandLine.Split(' ')` path. + - What's unclear: whether the owner considers `AddCommandLine()` in-scope for SRC-02 or only the binder `Parse`. + - Recommendation: change `AddCommandLine()` to `Environment.GetCommandLineArgs()` (small, low-risk, and it makes the headline "quoted value with spaces" criterion true end-to-end for the convenience method). Flag for the discuss/plan step; treat as in-scope unless the owner defers it. + +2. **Uniform `8.0.0` vs latest-patch-per-package for the net8 floor?** (see Standard Stack note) — recommend latest-patch (Option 1) to match the decision wording; owner may prefer uniform `8.0.0`. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| .NET SDK | build/test/restore | ✓ | 10.0.100 (pinned in `src/global.json`) | — | +| net8.0 runtime/targeting | net8 test target + PKG-02 restore | ✓ (CI installs `8.0.x`) | 8.0.x | CI provides via `setup-dotnet` | +| NuGet.org access | PKG-02 restore, PKG-01 unlist | ✓ | — | — | +| `dotnet nuget delete` (unlist) | PKG-01 D-02b | ✓ (SDK CLI) | — | nuget.org web UI | + +**Missing dependencies with no fallback:** none. +**Missing dependencies with fallback:** none blocking. (The local machine may lack the net8.0 **apphost** pack, but test projects set `UseAppHost=false` and libraries don't need it; CI installs the 8.0 runtime for the net8 test target.) + +## Validation Architecture + +> nyquist_validation is enabled — this section maps each success criterion to an automated check. + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | TUnit 1.58.0 on Microsoft.Testing.Platform | +| Config file | `src/global.json` (`"test": { "runner": "Microsoft.Testing.Platform" }`) | +| Quick run command | `cd src && dotnet test SimpleSettings.slnx -c Release --treenode-filter "/*/*/ArgumentsTests/*"` | +| Full suite command | `cd src && dotnet restore SimpleSettings.slnx && dotnet build SimpleSettings.slnx -c Release --no-restore -p:ContinuousIntegrationBuild=true && dotnet test SimpleSettings.slnx -c Release --no-build` | + +### Phase Requirements → Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| API-01 | `SettingsHolder` internal; build + suite green on both TFMs | build + suite | full suite command above | ✅ (green suite is the check; no new test needed — surface change is compile-verified by `InternalsVisibleTo`) | +| PKG-01 | `Core.AspNet` removed; solution builds; no test references it | build | `dotnet build SimpleSettings.slnx -c Release` | ✅ (build-green is the check) | +| PKG-02 | net8 resolves `8.0.x`, net10 resolves `10.0.9` | restore + assets inspection | restore, then assert `obj/**/project.assets.json` `targets["net8.0"]` shows `Microsoft.Extensions.Configuration/8.0.0` and **not** `10.` ; `targets["net10.0"]` shows `10.0.9` | ❌ Wave 0 (add a restore-assertion script / doc step) | +| SRC-02 | `--k=v`, `--k:v`, `--k v`, `--k "v with spaces"`, prefixed-next-token = new key, arg[0] skipped/kept per option | unit | `dotnet test ... --treenode-filter "/*/*/ArgumentsTests/*"` (extend) or a new `CommandLineParseTests` | ⚠️ Partial — `ArgumentsTests.cs` exists; add space-separated + arg[0] + prefixed-next-token cases | + +### Sampling Rate +- **Per task commit:** targeted `--treenode-filter` for the touched test class. +- **Per wave merge:** full suite on both TFMs (CI runs net8 + net10). +- **Phase gate:** full suite green + PKG-02 restore-assertion green before `/gsd-verify-work`. + +### PKG-02 restore/floor check (the one non-suite validation) +```bash +cd src +dotnet restore SimpleSettings.slnx +# assert net8 floor and net10 pin from the core library's assets: +python3 - <<'PY' +import json,glob,sys +ok=True +for f in glob.glob("Core/ExistForAll.SimpleSettings/obj/project.assets.json"): + a=json.load(open(f)) + for tfm,libs in a["targets"].items(): + for k in libs: + if k.lower().startswith("microsoft.extensions.configuration/"): + v=k.split("/")[1] + bad = (tfm.startswith("net8") and v.startswith("10.")) or (tfm.startswith("net10") and not v.startswith("10.")) + print(tfm, k, "BAD" if bad else "ok"); ok = ok and not bad +sys.exit(0 if ok else 1) +PY +``` +*(This exact technique was used during research to verify the conditional-`PackageVersion` shape.)* + +### Wave 0 Gaps +- [ ] Extend `src/Tests/.../Binders/CommandLine/ArgumentsTests.cs` (or add `CommandLineParseTests.cs`) — covers SRC-02: `--k v`, `--k "v with spaces"` (single token), `--k --other` (prefixed next → new key, no value), `SkipFirstArgument` true/false. +- [ ] Add the PKG-02 restore-assertion step above to the plan's verification (no framework install needed — `python3` is present). +- [ ] Framework install: none — TUnit + SDK already present. + +## Security Domain + +> security_enforcement enabled, ASVS L1. This phase is mostly surface-reduction (net security-positive). + +### Applicable ASVS Categories +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | no | — | +| V3 Session Management | no | — | +| V4 Access Control | no | — | +| V5 Input Validation | partial | CLI binder parses untrusted `string[]` into string config values only — no eval/injection sink; values flow to typed conversion which is already secret-safe (SEC-01). No new validation risk. | +| V6 Cryptography | no | — | +| V14 Config & Dependencies | yes | Per-TFM floors use Microsoft first-party versions; net8 floors (`8.0.0/8.0.1/8.0.2`) carry no known critical advisory for these specific packages. Lowering the floor does not add a vulnerable version; consumers can still float up. | + +### Known Threat Patterns for this stack +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| Reducing `SettingsHolder` visibility | (Info disclosure ↓) | API-01 shrinks public surface — positive | +| Dependency floor float | Supply chain / Tampering | First-party packages only; verified versions from nuget.org flatcontainer; CI restores from `api.nuget.org` | +| CLI arg parsing of untrusted input | Tampering | String-only config values; no command execution; existing secret-safe exception invariant (SEC-01) unchanged | + +**Net effect:** security-neutral-to-positive. No `block_on: high` findings. + +## Sources + +### Primary (HIGH confidence) +- **Live `dotnet restore` + `dotnet build /p:TreatWarningsAsErrors=true`** of an isolated `net8.0;net10.0` project with conditional `PackageVersion` — proved NU1504-free and confirmed per-TFM resolution (net8→8.0.0/8.0.1, net10→10.0.9). +- **nuget.org flatcontainer index** (`api.nuget.org/v3-flatcontainer//index.json`) — exact `8.0.x` / `10.0.x` version lists per package; `Core.AspNet` published-version list. +- **dotnet/runtime `CommandLineConfigurationProvider.cs`** (`main`, raw GitHub) — authoritative parse algorithm quoted for parity. +- **Codebase reads/greps** — `SettingsHolder`/`ISettingsHolder`/`SettingsCollection`/`Info.cs`, `Directory.Packages.props`, `SimpleSettings.slnx`, `UnitTests.csproj`, `Core.AspNet/Environments.cs`, `CommandLineSettingsBinder.cs`, `SettingsBuilderFactoryExtensions.cs`, `ArgumentsTests.cs`; no PublicAPI analyzer present. + +### Secondary (MEDIUM confidence) +- WebFetch summary of the CommandLine provider (cross-checked against the raw source quote above). + +### Tertiary (LOW confidence) +- None. + +## Metadata + +**Confidence breakdown:** +- Standard stack / versions: HIGH — every version verified against nuget.org on 2026-07-14. +- PKG-02 mechanics: HIGH — verified by live restore, not just docs. +- CLI parity: HIGH — quoted from the runtime source; divergence from Microsoft explicitly noted. +- PKG-01 removal safety: HIGH — dead reference confirmed by grep; NuGet publish state confirmed by API. +- Unlisting handling: MEDIUM — standard practice, but low-stakes owner choice (A1). + +**Research date:** 2026-07-14 +**Valid until:** 2026-08-13 (versions may gain new servicing patches; re-check `8.0.x`/`10.0.x` if planning slips a month). From 467fbccd187ebe447fa753cc9eb7f3cd4d69cf07 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 14:15:50 +0300 Subject: [PATCH 04/21] =?UTF-8?q?docs:=20refresh=20session=20handoff=20(Ph?= =?UTF-8?q?ase=203=20=E2=80=94=20discuss+research=20done,=20planner=20next?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SESSION-HANDOFF.md | 98 ++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 46 deletions(-) diff --git a/SESSION-HANDOFF.md b/SESSION-HANDOFF.md index 205adec..6d2f830 100644 --- a/SESSION-HANDOFF.md +++ b/SESSION-HANDOFF.md @@ -4,65 +4,71 @@ _Last updated: 2026-07-14 · owner: Guy Ludvig (guy@frontegg.com)_ ## TL;DR **GSD is the source of truth** (`.planning/`); `FIX-PLAN.md` is frozen historical reference only. -**PR #30 merged** (squash, via `guy-lud`) — "GSD planning cutover + Phase 2 binding-correctness engine test hardening". `master` @ **`67aa72f`**, clean, **no open PRs, no work branch**. Suite **94 tests net10** (CI runs net8 + net10). +Phases **1 & 2 are complete and merged** (PR #30 squash → `master` @ `67aa72f`). `master` is now @ **`8750359`** (a follow-up commit that tracks the `.claude/`/`.codex/` tooling + a handoff refresh — pushed, so it publishes a throwaway alpha; the "remove later" cleanup is still pending). -Milestone **v2.0.0** has 5 phases. **Phase 1** (S1 #27 / C2 #28 exception work) ✓ and **Phase 2** (engine test hardening) ✓ are complete and merged. **Next: Phase 3 — Public Surface, Packaging & Binder Cleanup.** - -Still **pre-stable** (no `v*` tag; only auto-alphas) — breaking changes are free, which is the point of Phase 3 (make `SettingsHolder` internal, decide `List` support, etc.) before the first `v2.0.0-beta`. +**Active work: Phase 3 — Public Surface, Packaging & Binder Cleanup**, on feature branch **`gsd/phase-3-public-surface-packaging-binder-cleanup`** (forked off `master` @ `8750359`). **Discuss is DONE** (CONTEXT + DISCUSSION-LOG committed) and **research is DONE** (`03-RESEARCH.md`, committed `d8e7a89`, HIGH confidence). **Planning resumes at the planner step** — re-run `/gsd-plan-phase 3` and it auto-uses the existing research. Suite **94 tests net10** (CI net8 + net10). ## Do this first (new session) -1. **Verify git state** (`git log -3`, `gh pr list`) — expect `master` @ `67aa72f` (PR #30 merged), **no open PRs**, clean tree except this uncommitted handoff refresh + untracked `.claude/`/`.codex/`. -2. **Run `/gsd-progress`** to see the roadmap and reconcile STATE.md. ⚠️ `.planning/STATE.md` has minor cosmetic drift from the phase-2 completion write (`completed_phases: 1` should read 2; `status: verifying` is stale) — GSD reconciles it on the next workflow command; don't hand-edit + commit it to `master` (see the doc-only-alpha gotcha). -3. **Start Phase 3:** `/gsd-discuss-phase 3` (recommended — no CONTEXT.md exists yet) → `/gsd-plan-phase 3` → `/gsd-execute-phase 3`. Ship it on a **feature branch + PR** (see gotchas), not on `master`. +1. **Verify git state** (`git log -3`, `git branch --show-current`, `gh pr list`): expect to be on **`gsd/phase-3-…`**, `master` @ `8750359` (pushed), PR #30 merged, no open PRs. Working tree may carry an uncommitted handoff refresh + untracked build noise. +2. **Check Phase 3 planning progress:** `ls .planning/phases/03-public-surface-packaging-binder-cleanup/` + - `03-CONTEXT.md` + `03-DISCUSSION-LOG.md` are committed (discuss done). + - **If `03-RESEARCH.md` exists and NO `03-*-PLAN.md` yet** → resume planning: **`/gsd-plan-phase 3`** — it auto-uses the existing RESEARCH.md (no re-prompt) and continues: VALIDATION.md → pattern-mapper → gsd-planner → gsd-plan-checker (revision loop, max 3) → coverage gates → PLANNED. + - **If `03-*-PLAN.md` files exist** → planning finished; run **`/gsd-execute-phase 3`**. +3. **Stay on the feature branch.** Ship Phase 3 as one PR to `master` via `guy-lud` after it verifies — never commit docs directly to `master` (burns an alpha). + +## Phase 3 — locked decisions (from discuss; full detail + file:line refs in `03-CONTEXT.md`) +- **API-01:** `SettingsHolder` (`SettingsHolder.cs:5`) `public class` → **`internal sealed`**. `ISettingsHolder` already internal; only `SettingsCollection` uses it; tests keep access via `InternalsVisibleTo` (`Info.cs:3-4`). +- **PKG-01:** **DROP the `Core.AspNet` package** (its only type is `internal static Environments` → zero public surface). Remove `SimpleSettings.slnx:13` entry + `UnitTests.csproj:19` ProjectReference; confirm no test uses `Environments`. +- **PKG-02:** **Float `Microsoft.Extensions.*` per-TFM** in `Directory.Packages.props:8-12` (all four): net8 → **latest `8.0.x`**, net10 → `10.0.x`, via CPM conditional `PackageVersion`. +- **SRC-02:** CLI binder (`CommandLineSettingsBinder.cs`): **add space-separated `--key value`** (lookahead; a prefixed next token = new key) + keep inline `=`/`:`; **skip `arg[0]` by default** via a new `SkipFirstArgument` option (default true) on `CommandLineSettingsBinderOptions`. + +## Phase 3 — verified research findings (feed the planner; full detail in `03-RESEARCH.md`) +- **PKG-02 (verified by live restore, 0 warnings, no NU1504):** per-TFM conditional `` works. **Exact net8 floors differ per package:** `Configuration`=`8.0.0`, `Configuration.Json`=`8.0.1`, `DependencyInjection`=`8.0.1`, `DependencyInjection.Abstractions`=`8.0.2`. net10 stays `10.0.9`. `CentralPackageTransitivePinningEnabled` stays off. +- **PKG-01:** `Core.AspNet` **is published on NuGet** (`1.0.0` + a `2.0.0-alpha.0.N` run) → pair removal with **unlisting** the alphas (owner choice, low-stakes). The `UnitTests.csproj:19` ProjectReference is **DEAD** (the package's `Environments` is internal w/ no `InternalsVisibleTo`; tests use a local duplicate) → safe to remove. +- **API-01:** flip is self-contained — no test references `SettingsHolder`; no PublicAPI analyzer/baseline in the repo. +- **SRC-02:** our lookahead **deliberately diverges** from Microsoft's provider (which consumes the next token unconditionally). The `arg[0]` skip is needed because `AddCommandLine()` uses `Environment.CommandLine.Split(' ')` (includes exe path + quote-unsafe). Existing `ArgumentsTests` stay green under `SkipFirstArgument=true` (fixture's leading space makes `args[0]` empty). +- **Open questions for planning:** + 1. **(recommend YES, in-scope)** Switch `AddCommandLine()` from `Environment.CommandLine.Split(' ')` → `Environment.GetCommandLineArgs()` so the "quoted value with spaces" criterion holds end-to-end for that entry point. + 2. **(resolved)** net8 floor = latest-patch-per-package (above), not a uniform `8.0.0`. ## Current state -- On **`master`** @ `67aa72f`, clean. The merged branch `chore/gsd-ownership-cutover` is deleted locally; **it still exists on `origin`** (GitHub didn't auto-delete) — optional cleanup: `git push origin --delete chore/gsd-ownership-cutover` (needs the `guy-lud` push identity; origin already uses the alias). -- **Phases 1–2 complete + merged.** Build clean (0 warnings, both TFMs). Suite **94 net10** (Phase 2 added +7 engine-core + +3 scalar-converter over the prior 84). -- A **`gh-pages`** branch holds benchmark data (`dev/bench/`); do **not** delete it — the allocation baseline lives there. +- On **`gsd/phase-3-public-surface-packaging-binder-cleanup`**. Phase-3 commits so far: `e945d99` (03-CONTEXT + 03-DISCUSSION-LOG), `6cbf370` (STATE record-session). Research step ran after that. +- **Phases 1–2 merged.** Build clean (0 warnings, both TFMs). Suite **94 net10**. +- The merged branch `chore/gsd-ownership-cutover` is deleted locally but **still exists on `origin`** (optional cleanup via `guy-lud`). +- A **`gh-pages`** branch holds the benchmark baseline (`dev/bench/`) — do **not** delete it. -## What shipped in Phase 2 (this session — test-only, zero `src/` production changes) -- **TEST-01** `Core/ValuesPopulatorTests.cs` — binder last-writer-wins, earlier-survives-when-later-silent, attribute `DefaultValue` survives. -- **TEST-02** `Core/TypeConverterTests.cs` — `null→0`, `int?` null→null, `"42"→42`, `ConverterType` bypasses the collection converter (via the internal `TypeConverter.CreateConversion` seam). -- **TEST-03** `Conversion/ScalarConversionTests.cs` — scalar `Uri` positive, `DateTime` positive, one format-mismatch negative (exception **type only** — redaction stays owned by `ExceptionRedactionTests`). -- **ENG-01** verify-only — confirmed `SettingsClassGenerator._generationGate` + concurrency stress tests satisfy success criterion #4 (shipped in #29/T7). The load-bearing proof is `..._IsRaceFree` (Barrier, genuine contention), not `..._ReturnsSingleSharedType` (timing-dependent). No code change. -- **COLL-01 owner-deferred** — the `List`/`IList`/`ICollection` broaden-vs-document+throw decision is held; recorded in the plan's `` section, `PROJECT.md`, and `02-VERIFICATION.md`. Not a gap. -- **Quality gates:** plan reviewed up front by `dotnet-architect` + `performance-analyst` + `security-auditor` (all PASS, no blockers); finished tests reviewed by `code-reviewer` (clean, 2 cosmetic NITs); `gsd-verifier` **passed 10/10 must-haves**. +## What shipped in Phases 1–2 (merged via PR #30) +- **Phase 1** — S1 secret redaction (#27), C2 public exception hierarchy (#28): `SettingsPropertyValueException` carries no value / chains no inner; all exceptions derive from `public abstract SimpleSettingsException`. +- **Phase 2** — engine test hardening (test-only): `ValuesPopulatorTests` (TEST-01 precedence/default), `TypeConverterTests` (TEST-02 null/nullable/ConverterType), `ScalarConversionTests` (TEST-03 Uri/DateTime), ENG-01 verify-only (generator concurrency gate from #29 confirmed; `..._IsRaceFree` is the load-bearing proof). COLL-01 owner-deferred. -## Next priorities — Phase 3: Public Surface, Packaging & Binder Cleanup -Goal: lock the public API surface and packaging before the first beta. Requirements (from ROADMAP / PROJECT.md Active): -1. **API-01 (A5)** — make `SettingsHolder`/`ISettingsHolder` internal *(breaking)*. -2. **PKG-01 (A3)** — `Core.AspNet` exposes a public type (`Environments` public) or drop the package. -3. **PKG-02 (A4)** — float `Microsoft.Extensions.*` floor per-TFM (`8.0.x` for net8) or justify the pin. -4. **SRC-02 (A6)** — command-line binder: parse quoted values with spaces, skip `arg[0]`. -5. **AOT-01 (A1, HIGH)** — annotate reflection entry points (`[RequiresDynamicCode]`/`[RequiresUnreferencedCode]`) or document the AOT/trim limitation before stable. -6. **DOC-01** — refresh README to canonical `ExistForAll.SimpleSettings` naming + current links. -7. **REL-01** — cut the first `v2.0.0-beta` once the breaking changes are batched; suite green net8 + net10. -- **COLL-01 (C1)** still pending the owner's broaden-vs-throw decision — surface it when Phase 3 is discussed. +## Remaining roadmap (5 phases; 1–2 done) +- **Phase 3 (active):** API-01, PKG-01, PKG-02, SRC-02 — above. +- **Phase 4:** AOT-01 (reflection/AOT-trim annotations), DOC-01 (README refresh). +- **Phase 5:** REL-01 (cut the first `v2.0.0-beta`; suite green net8 + net10). +- **Owner-deferred / held:** COLL-01 (`List` support), D1 Validations, D2 EqualityCompererCreator. ## How releasing works (durable) -- **`ci.yml`** — PRs to `master`: build + test (net8.0 + net10.0). **`release.yml`**: push to `master` → auto-publishes a MinVer height-based `-alpha`; manual **Release** (`workflow_dispatch`, `channel` beta/rc/stable + `bump`) tags `v*`, publishes, creates a GitHub Release (`dry_run: true` previews). -- **`benchmark.yml`** — push to `master` + PRs: runs BDN, gates PRs on allocation regressions (baseline in `gh-pages`). -- **Versioning = MinVer**, tag prefix `v`, baseline **2.0.0**, keyless publish via NuGet Trusted Publishing (OIDC). Workflows use `SOLUTION=SimpleSettings.slnx`. **Every `master` push publishes an alpha → everything goes through PRs.** +- **`ci.yml`** — PRs to `master`: build + test (net8.0 + net10.0). **`release.yml`**: push to `master` → auto-publishes a MinVer height-based `-alpha` (NO `paths` filter — every push, incl. docs, publishes). Manual **Release** (`workflow_dispatch`, channel beta/rc/stable + bump) tags `v*`. +- **`benchmark.yml`** — push to `master` + PRs: BDN, gates PRs on allocation regressions (baseline in `gh-pages`). +- **Versioning = MinVer**, tag prefix `v`, baseline **2.0.0**, keyless NuGet Trusted Publishing (OIDC). Workflows use `SOLUTION=SimpleSettings.slnx`. **Everything goes through PRs.** ## Gotchas a new session MUST know -- **TUnit test filtering:** `dotnet test --filter "*Name*"` is **rejected** by Microsoft.Testing.Platform/TUnit — exits **5** with zero tests run (looks green at a glance). Use `--treenode-filter "/*/*/ClassNameTests/*"` or run unfiltered and confirm the expected methods appear. Several `*-PLAN.md` `` blocks still carry the wrong `--filter` form. (Both Phase-2 executors hit this.) See project memory `[[simplesettings-test-stack]]`. -- **Run `dotnet` from `src/`** (global.json opts into Microsoft.Testing.Platform for TUnit). net10 runtime only locally → net8 is **build-only** locally; CI runs both. Don't `cd ` before `dotnet`. Single project on net10: `dotnet test --framework net10.0 --no-build` (build first). -- **Pushing / PRs:** active `git`/`gh` identity (`guy-frontegg`) is **read-only** here; push/PR/merge via **`guy-lud`**. `origin` already uses SSH alias **`github-guy-lud`**, so **`git push` already uses guy-lud**. For `gh` writes: `gh auth switch --user guy-lud`, then switch back to `guy-frontegg` after. See project memory `[[simplesettings-push-access]]`. -- **Never commit docs to `master`:** `release.yml` fires on *every* `master` push with **no `paths` filter**, so a doc-only push burns a throwaway `-alpha`. **Wrap ritual:** refresh THIS file so it rides the next work branch; if everything is merged (no work branch), **leave it uncommitted** for the next session's first branch to carry — which is exactly its current state. See project memory `[[simplesettings-handoff-workflow]]`. -- **GSD branching:** `.planning/config.json` has `branching_strategy: none`, so `/gsd-execute-phase` commits on the *current* branch. Do NOT execute Phase 3 on `master` — create a feature branch first (that's how Phase 2 shipped), then PR. See `[[simplesettings-gsd-source-of-truth]]`. -- **Review workflow (per `[[dotnet-review-workflow]]`):** plan → review the plan with `dotnet-architect`/`performance-analyst`/`security-auditor` → implement → review finished code with `code-reviewer`. This session **all four kit agents fired cleanly** with real tool calls; historically they've been intermittently flaky (return a preamble with 0 tool calls), so still verify each returns substance — the `/code-review` skill is a reliable fallback for the code step. -- **Benchmarks:** run from `src/` — `dotnet run -c Release --project performance/ExistForAll.SimpleSettings.Benchmark -- --filter --job short`. +- **TUnit test filtering:** `dotnet test --filter "*Name*"` is **rejected** by Microsoft.Testing.Platform/TUnit — exits **5**, zero tests (looks green at a glance). Use `--treenode-filter "/*/*/ClassNameTests/*"` or run unfiltered. Several `*-PLAN.md` `` blocks still carry the wrong `--filter` form. See `[[simplesettings-test-stack]]`. +- **Run `dotnet` from `src/`** (global.json → Microsoft.Testing.Platform). net10 runtime only locally → net8 build-only locally; CI runs both. Don't `cd ` before `dotnet`. +- **Pushing / PRs:** active `git`/`gh` identity (`guy-frontegg`) is **read-only** here; push/PR/merge via **`guy-lud`**. `origin` uses SSH alias **`github-guy-lud`** → `git push` already uses guy-lud. For `gh` writes: `gh auth switch --user guy-lud`, then switch back to `guy-frontegg`. See `[[simplesettings-push-access]]`. +- **Never commit docs to `master`** (release.yml has no `paths` filter → burns an alpha). Wrap ritual: refresh THIS file so it rides the current work branch (currently committed on `gsd/phase-3-…`). See `[[simplesettings-handoff-workflow]]`. +- **GSD branching:** `config.json` `branching_strategy: none` → `/gsd-execute-phase` commits on the *current* branch. Keep Phase 3 on its feature branch; do NOT execute on `master`. See `[[simplesettings-gsd-source-of-truth]]`. +- **Review workflow (`[[dotnet-review-workflow]]`):** plan → review the plan with `dotnet-architect`/`performance-analyst`/`security-auditor` → implement → review finished code with `code-reviewer`. All four kit agents fired cleanly recently; still verify each returns real tool calls; `/code-review` skill is a reliable fallback. - Commits/PRs here **omit** the Co-Authored-By / Generated-with trailer (project preference). ## Key decisions & context (carry forward) -- **Exception-redaction invariant (S1+C2, locked).** `SettingsPropertyValueException` never carries the bound value and never chains an inner (ctor takes the failure `Type`, not the `Exception`). `SettingsBindingException` stores primitives, not the `BindingContext`. `SettingsPropertyNullException` is the value-free "required missing" case. Phase 2 tests were explicitly forbidden from re-asserting/weakening this. -- **Exception hierarchy (C2, done #28).** All library exceptions derive from `public abstract SimpleSettingsException` in the root namespace; a reflection invariant test enforces it. -- **Generator concurrency (ENG-01/T7, done #29).** `SettingsClassGenerator` serializes ALL generation behind one `_generationGate` (double-checked locking; warm path lock-free). Do NOT "optimize" to `Lazy`-per-type — `Reflection.Emit` isn't thread-safe, so concurrent `DefineType` of distinct interfaces also races the shared `ModuleBuilder`. Stress tests guard this. -- **Benchmark tracking gates on ALLOCATIONS, not time.** `gh-pages` (`dev/bench/`) holds the baseline. -- **C3 resolved — provider-level cache (option 2).** Reload/`IOptionsMonitor` is future "option 3". -- **Validations (D1) + `EqualityCompererCreator` (D2) — HELD, do NOT delete** (dead today, reserved for feature work). -- **Pre-stable window:** no `v*` stable tag; breaking changes free until the first `v2.0.0-beta`. +- **Exception-redaction invariant (S1+C2, locked).** `SettingsPropertyValueException` never carries the bound value / never chains an inner; `SettingsBindingException` stores primitives; `SettingsPropertyNullException` = value-free "required missing". Don't weaken. +- **Generator concurrency (ENG-01/T7, #29).** One `_generationGate` over all generation (double-checked lock; warm path lock-free). Don't switch to `Lazy`-per-type (distinct-interface `DefineType` also races the shared `ModuleBuilder`). +- **Benchmark tracking gates on ALLOCATIONS, not time** (`gh-pages` baseline). +- **C3** = provider-level cache (option 2); reload/`IOptionsMonitor` is future "option 3". +- **Pre-stable window:** no `v*` tag; breaking changes free until the first `v2.0.0-beta`. ## Minor tracked follow-ups (non-blocking) -- **Cosmetic NITs** (from Phase 2 code review) in `Core/TypeConverterTests.cs`: add `using System;` (drop `System.Type` qualification); optionally rename `Convert_NullForNonNullableValueType_ReturnsTypeDefault` → `..._WithoutAttribute_...`. -- **REQUIREMENTS.md traceability:** 13 brownfield baseline IDs (`BIND-01…NAME-01`) appear in the body but not the traceability table (pre-existing; surfaces in `/gsd-progress` and `/gsd-audit-uat`). +- **`.claude/`/`.codex/` are tracked on `master`** (commit `8750359`, "remove later"). Cleanup: add to `.gitignore` + `git rm --cached` — do it on a **branch/PR** (on `master` = another alpha). +- **Cosmetic NITs** (Phase 2 code review) in `Core/TypeConverterTests.cs`: add `using System;`; optionally rename `Convert_NullForNonNullableValueType_ReturnsTypeDefault` → `..._WithoutAttribute_...`. +- **REQUIREMENTS.md traceability:** 13 brownfield baseline IDs (`BIND-01…NAME-01`) are in the body but not the traceability table (pre-existing; surfaces in `/gsd-progress`). From f4053e379c46a2e96363826b1539c3b8abdc5e68 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 14:24:53 +0300 Subject: [PATCH 05/21] docs(phase-3): add validation strategy --- .../03-VALIDATION.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-VALIDATION.md diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VALIDATION.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VALIDATION.md new file mode 100644 index 0000000..516791c --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VALIDATION.md @@ -0,0 +1,76 @@ +--- +phase: 3 +slug: public-surface-packaging-binder-cleanup +status: draft +nyquist_compliant: false +wave_0_complete: false +created: 2026-07-14 +--- + +# Phase 3 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} | +| **Config file** | {path or "none — Wave 0 installs"} | +| **Quick run command** | `{quick command}` | +| **Full suite command** | `{full command}` | +| **Estimated runtime** | ~{N} seconds | + +--- + +## Sampling Rate + +- **After every task commit:** Run `{quick run command}` +- **After every plan wave:** Run `{full suite command}` +- **Before `/gsd-verify-work`:** Full suite must be green +- **Max feedback latency:** {N} seconds + +--- + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +--- + +## Wave 0 Requirements + +- [ ] `{tests/test_file.py}` — stubs for REQ-{XX} +- [ ] `{tests/conftest.py}` — shared fixtures +- [ ] `{framework install}` — if no framework detected + +*If none: "Existing infrastructure covers all phase requirements."* + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| {behavior} | REQ-{XX} | {reason} | {steps} | + +*If none: "All phase behaviors have automated verification."* + +--- + +## Validation Sign-Off + +- [ ] All tasks have `` verify or Wave 0 dependencies +- [ ] Sampling continuity: no 3 consecutive tasks without automated verify +- [ ] Wave 0 covers all MISSING references +- [ ] No watch-mode flags +- [ ] Feedback latency < {N}s +- [ ] `nyquist_compliant: true` set in frontmatter + +**Approval:** {pending / approved YYYY-MM-DD} From 6d4dc5012bfda24126d0aab48b27e4c86f958b52 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 14:44:34 +0300 Subject: [PATCH 06/21] docs(03): create phase plan (public surface, packaging & binder cleanup) --- .planning/ROADMAP.md | 7 +- .../03-01-PLAN.md | 231 ++++++++++++++++++ .../03-02-PLAN.md | 216 ++++++++++++++++ 3 files changed, 452 insertions(+), 2 deletions(-) create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-01-PLAN.md create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 0e16299..83c3bbd 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -76,7 +76,10 @@ value: config → typed settings maps accurately, and never leaks a secret doing 3. A net8 consumer is no longer transitively forced onto `Microsoft.Extensions.* 10.x` (per-TFM floor), or the pin is documented with justification. 4. A quoted command-line value containing spaces binds correctly and the executable path (`arg[0]`) is skipped. -**Plans**: TBD +**Plans**: 2 plans + +- [ ] 03-01-PLAN.md — Public surface & packaging cleanup: SettingsHolder → internal sealed (API-01), drop dead Core.AspNet package (PKG-01), per-TFM Microsoft.Extensions.* floor (PKG-02) [Wave 1] +- [ ] 03-02-PLAN.md — Command-line binder cleanup: SkipFirstArgument option + space-separated `--k v` lookahead + arg[0] skip + AddCommandLine tokenization (SRC-02) [Wave 2, depends on 03-01] ### Phase 4: AOT/Trim Honesty & Documentation @@ -114,6 +117,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 |-------|----------------|--------|-----------| | 1. Exception Safety & Public Hierarchy | n/a (shipped) | ✓ Complete | 2026-07-14 (#27/#28) | | 2. Binding Correctness & Engine Test Hardening | 2/2 | Complete | 2026-07-14 | -| 3. Public Surface, Packaging & Binder Cleanup | 0/TBD | Not started | - | +| 3. Public Surface, Packaging & Binder Cleanup | 0/2 | Not started | - | | 4. AOT/Trim Honesty & Documentation | 0/TBD | Not started | - | | 5. First v2.0.0-beta Release | 0/TBD | Not started | - | diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-PLAN.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-PLAN.md new file mode 100644 index 0000000..e599ac8 --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-PLAN.md @@ -0,0 +1,231 @@ +--- +phase: 03-public-surface-packaging-binder-cleanup +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs + - src/Directory.Packages.props + - src/SimpleSettings.slnx + - src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj + - src/Core/ExistForAll.SimpleSettings.Core.AspNet/ +autonomous: true +requirements: [API-01, PKG-01, PKG-02] +user_setup: + - service: nuget.org + why: "PKG-01 (D-02b) — after Core.AspNet is removed from the repo the already-published 2.0.0-alpha.0.* prereleases stop updating but remain listed. Owner-optional registry hygiene; NOT automated by this plan." + dashboard_config: + - task: "Unlist the published ExistForAll.SimpleSettings.Core.AspNet prerelease alphas (dotnet nuget delete -s https://api.nuget.org/v3/index.json --non-interactive — unlists, does not delete). Requires the guy-lud publishing account." + location: "NuGet.org (or `dotnet nuget delete`) under the guy-lud account" + +must_haves: + truths: + - "SettingsHolder no longer appears on the public API surface — it compiles as internal sealed, reachable from tests only via InternalsVisibleTo (SC #1)." + - "Core.AspNet is absent from the solution and no project references it; the solution builds without it (SC #2)." + - "A net8 build resolves Microsoft.Extensions.* at 8.0.x (not 10.x); a net10 build resolves 10.0.9 (SC #3)." + - "The full test suite stays green on net8 and net10 after all three edits (SC #1)." + artifacts: + - "src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs — internal sealed class SettingsHolder" + - "src/Directory.Packages.props — per-TFM conditional PackageVersion items for the four Microsoft.Extensions.* packages" + - "src/SimpleSettings.slnx — no Core.AspNet Project entry" + - "src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj — no Core.AspNet ProjectReference" + key_links: + - "Info.cs InternalsVisibleTo(UnitTests, Benchmark) keeps the internal-flipped SettingsHolder reachable from the test + benchmark assemblies." + - "Directory.Packages.props $(TargetFramework) conditions are evaluated per-TFM by NuGet restore → the net8.0 target in project.assets.json lists 8.0.x, the net10.0 target lists 10.0.9." + - "The test project's local duplicate Core/AspNet/Environments.cs (namespace ...UnitTests.Core.AspNet) is independent of the removed package and keeps the Core/AspNet tests compiling." +--- + + +Trim the public surface and correct packaging for the pre-beta cleanup: make `SettingsHolder` +internal (API-01, D-01), remove the dead `Core.AspNet` package (PKG-01, D-02/D-02b), and float +the `Microsoft.Extensions.*` dependency floor per-TFM so net8 consumers are no longer forced onto +10.x (PKG-02, D-03). All three are deliberate breaking/packaging changes, batched pre-`v2.0.0-beta`. + +Purpose: satisfy Phase 3 success criteria #1, #2, #3 — meaningful, correctly-scoped public + +package surface, with the suite green on both TFMs. +Output: an internal-sealed `SettingsHolder`, a Core.AspNet-free solution graph, and per-TFM +conditional `PackageVersion` items — no runtime-logic changes. + + + +@/Users/guyludvig/frontegg/development/open-source/SimpleSettings/.claude/gsd-core/workflows/execute-plan.md +@/Users/guyludvig/frontegg/development/open-source/SimpleSettings/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md +@.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md +@.planning/phases/03-public-surface-packaging-binder-cleanup/03-PATTERNS.md + +# Source of truth for this plan's edits: +@src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs +@src/Core/ExistForAll.SimpleSettings/ISettingsHolder.cs +@src/Core/ExistForAll.SimpleSettings/Info.cs +@src/Directory.Packages.props +@src/SimpleSettings.slnx +@src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj + + + +Symbols/paths this plan creates, changes, or removes (read by drift verification to exclude +newly-created or intentionally-removed surface): + +- CHANGED symbol: `ExistForAll.SimpleSettings.SettingsHolder` — accessibility `public class` -> `internal sealed class` (removed from public API surface; API-01/D-01). +- REMOVED project/package: `ExistForAll.SimpleSettings.Core.AspNet` (directory `src/Core/ExistForAll.SimpleSettings.Core.AspNet/`, its `.csproj`, and its `Environments.cs`); removed from `src/SimpleSettings.slnx` and from `UnitTests.csproj` ProjectReference (PKG-01/D-02). +- NEW build items: per-TFM conditional `PackageVersion` items in `src/Directory.Packages.props` — two mutually-exclusive items (net8.0 / net10.0) per package for the four `Microsoft.Extensions.*` packages (PKG-02/D-03). Net8 floors: Configuration `8.0.0`, Configuration.Json `8.0.1`, DependencyInjection `8.0.1`, DependencyInjection.Abstractions `8.0.2`; net10 stays `10.0.9`. +- UNCHANGED (kept, do NOT touch): the test project's local duplicate `src/Tests/ExistForAll.SimpleSettings.UnitTests/Core/AspNet/Environments.cs` (namespace `...UnitTests.Core.AspNet`); TUnit/BenchmarkDotNet/MinVer `PackageVersion` items; `CentralPackageTransitivePinningEnabled` stays unset (off). + + + + + + Task 1: Flip SettingsHolder to internal sealed (API-01 / D-01) + src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs + + - src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs (the file being modified — current declaration on line 5 is `public class SettingsHolder : ISettingsHolder`) + - src/Core/ExistForAll.SimpleSettings/ISettingsHolder.cs (analog: already `internal interface` — mirror this accessibility) + - src/Core/ExistForAll.SimpleSettings/Info.cs (confirms InternalsVisibleTo -> UnitTests + Benchmark; no edit needed) + - src/Core/ExistForAll.SimpleSettings/SettingsCollection.cs (sole consumer; internal — confirms no external caller) + + + Per D-01, change the type declaration on line 5 of SettingsHolder.cs so the class is declared + internal and sealed (accessibility internal, add the sealed modifier, keep the `: ISettingsHolder` + base). Change only the accessibility and add sealed — leave the constructor, both get-only + properties (SettingsType, SettingsImplementation), the block-scoped namespace, and the + `using System;` exactly as-is. Do NOT add or change any other file: ISettingsHolder is already + internal, the only consumer SettingsCollection is internal, and Info.cs already grants + InternalsVisibleTo to the UnitTests + Benchmark assemblies. There is no PublicAPI analyzer baseline + in the repo, so no surface file to update. + + + cd src && grep -q 'internal sealed class SettingsHolder' Core/ExistForAll.SimpleSettings/SettingsHolder.cs && dotnet build Core/ExistForAll.SimpleSettings/ExistForAll.SimpleSettings.csproj -c Release + + + - SettingsHolder.cs line 5 matches `internal sealed class SettingsHolder : ISettingsHolder`. + - The core project (`ExistForAll.SimpleSettings.csproj`) builds with 0 errors on both net8.0 and net10.0. + - No other file under src/Core/ExistForAll.SimpleSettings/ was modified for this task. + + SettingsHolder is internal sealed; the core library compiles; tests still reach it via InternalsVisibleTo (proven by the plan-level full suite). + + + + Task 2: Remove the dead Core.AspNet package (PKG-01 / D-02, D-02b) + src/SimpleSettings.slnx, src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj, src/Core/ExistForAll.SimpleSettings.Core.AspNet/ (removed) + + - src/SimpleSettings.slnx (line 13 is the Core.AspNet `` entry under the /Extensions/ folder — the one to remove) + - src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj (line 19 is the Core.AspNet `` to remove) + - src/Core/ExistForAll.SimpleSettings.Core.AspNet/Environments.cs (the only source in the package — internal static class, zero public surface, no InternalsVisibleTo) + - src/Tests/ExistForAll.SimpleSettings.UnitTests/Core/AspNet/Environments.cs (the test project's LOCAL duplicate, namespace `...UnitTests.Core.AspNet` — LEAVE it; the Core/AspNet tests use this, not the package) + - 03-RESEARCH.md §"PKG-01 — the three removal points" and §"Runtime State Inventory" (published-alphas / unlist context) + + + Per D-02, remove the Core.AspNet package from the build graph in three places, then delete the + project directory: + 1. Delete the Core.AspNet `` + entry from src/SimpleSettings.slnx. Leave the other project entries in the /Core/ and /Extensions/ + folders intact. + 2. Delete the Core.AspNet `` + from UnitTests.csproj. Leave the other three ProjectReferences (Binders, core, Extensions.GenericHost) intact. + 3. Delete the directory src/Core/ExistForAll.SimpleSettings.Core.AspNet/ in full (its .csproj, + Environments.cs, and stale bin/ + obj/). + Do NOT touch the test project's own Core/AspNet/Environments.cs (different namespace, local + duplicate — it is what the Core/AspNet tests consume; verified there is no + `using ExistForAll.SimpleSettings.Core.AspNet` anywhere in src/). Per D-02b, do NOT publish or + unlist anything from this task — the published 2.0.0-alpha.0.* alphas simply stop updating on + removal; unlisting is an owner-optional follow-up captured in this plan's user_setup (do not + automate a publish/unlist). + + + cd src && test ! -e Core/ExistForAll.SimpleSettings.Core.AspNet && test -f Tests/ExistForAll.SimpleSettings.UnitTests/Core/AspNet/Environments.cs && ! grep -rq "Core.AspNet" SimpleSettings.slnx Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj && dotnet restore SimpleSettings.slnx && dotnet build SimpleSettings.slnx -c Release --no-restore + + + - The directory `src/Core/ExistForAll.SimpleSettings.Core.AspNet/` no longer exists. + - `grep -rn "Core.AspNet"` over `src/SimpleSettings.slnx` and the UnitTests `.csproj` returns no matches. + - `src/Tests/ExistForAll.SimpleSettings.UnitTests/Core/AspNet/Environments.cs` still exists (local duplicate preserved). + - `dotnet restore` + `dotnet build SimpleSettings.slnx -c Release` succeed on both TFMs with 0 errors. + - The NuGet unlist step is documented in user_setup and NOT executed by this task. + + Core.AspNet is gone from the repo and solution graph; the solution restores and builds; the test project's local Environments duplicate is untouched; NuGet unlisting is left as an owner-optional follow-up. + + + + Task 3: Float Microsoft.Extensions.* floor per-TFM (PKG-02 / D-03) + src/Directory.Packages.props + + - src/Directory.Packages.props (lines 7-13 hold the four unconditional `Microsoft.Extensions.*` pins at 10.0.9 — these are what change) + - 03-PATTERNS.md §"Directory.Packages.props (config/CPM — PKG-02, D-03)" (the exact verified target ItemGroup) + - 03-RESEARCH.md §"Pattern 1: Per-TFM conditional PackageVersion" and §"Pitfall 1: NU1504" (why every item must be mutually-exclusively conditioned) + + + Per D-03, replace each of the four single unconditional `Microsoft.Extensions.*` PackageVersion + items (currently all at 10.0.9) with TWO mutually-exclusive conditional items — one gated on + `Condition="'$(TargetFramework)' == 'net8.0'"` at the net8 floor, one gated on + `Condition="'$(TargetFramework)' == 'net10.0'"` at Version 10.0.9. Use these exact net8 floors + (research-verified latest-patch-per-package, Option 1; each restores with 0 warnings): + Microsoft.Extensions.Configuration -> net8 8.0.0; Microsoft.Extensions.Configuration.Json -> net8 8.0.1; + Microsoft.Extensions.DependencyInjection -> net8 8.0.1; Microsoft.Extensions.DependencyInjection.Abstractions + -> net8 8.0.2. All four keep net10 = 10.0.9. Do NOT use a single shared MSBuild property (the net8 + patches differ per package, so the property-indirection form does not fit — use conditional items). + Do NOT add an unconditional item alongside a conditional one for the same package (that trips NU1504). + Do NOT set CentralPackageTransitivePinningEnabled (stays off — correct, only direct deps are pinned). + Leave the TUnit / BenchmarkDotNet / MinVer ItemGroups untouched. + NOTE (owner-resolvable, already decided): net8 floor = latest-patch-per-package (Option 1) per the + decision wording; a uniform 8.0.0 for all four (Option 2, friendliest-lowest floor) is also valid. + Ship Option 1 unless the owner prefers uniform 8.0.0. + + + cd src && dotnet restore SimpleSettings.slnx && dotnet build SimpleSettings.slnx -c Release --no-restore -p:TreatWarningsAsErrors=true && python3 -c 'import json,glob,sys; t=json.load(open(glob.glob("Core/ExistForAll.SimpleSettings/obj/project.assets.json")[0]))["targets"]; bad=[(f,k) for f,libs in t.items() for k in libs if k.lower().startswith("microsoft.extensions.") and ((f.startswith("net8") and not k.split("/")[1].startswith("8.")) or (f.startswith("net10") and not k.split("/")[1].startswith("10.")))]; print("mismatches:",bad); sys.exit(1 if bad else 0)' + + + - `Directory.Packages.props` has eight conditional `Microsoft.Extensions.*` items (two per package), each carrying a mutually-exclusive `$(TargetFramework)` Condition; no unconditional `Microsoft.Extensions.*` item remains. + - `dotnet build -p:TreatWarningsAsErrors=true` succeeds with 0 warnings (no NU1504 duplicate, no NU1605/NU1608 downgrade) on both TFMs. + - The assets check prints `mismatches: []` and exits 0 — the net8.0 target resolves `Microsoft.Extensions.*` at 8.0.x and the net10.0 target at 10.0.9. + + net8 consumers float to 8.0.x, net10 stays 10.0.9; restore + warnings-as-errors build clean on both TFMs; project.assets.json confirms per-TFM resolution. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| library public API → external consumers | `SettingsHolder` currently crosses this boundary as a public type it should not (API-01 closes it). | +| build/CI → NuGet.org restore | Package versions/floors are resolved across this boundary (PKG-02); a removed project stops publishing across it (PKG-01). | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-03-01 | Information Disclosure | `SettingsHolder` public surface | low | mitigate | API-01/D-01 makes it `internal sealed`, removing an internal DTO from the public surface (net surface-reduction; no new exposure). | +| T-03-02 | Tampering (supply chain) | `Microsoft.Extensions.*` net8 floor float (PKG-02) | low | mitigate | First-party packages only; net8 floors 8.0.0/8.0.1/8.0.1/8.0.2 verified against nuget.org flatcontainer (2026-07-14); no known critical advisory for these versions; consumers may still float up. CPM conditional items; `CentralPackageTransitivePinningEnabled` stays off; warnings-as-errors build catches NU1504/NU1605. | +| T-03-03 | Tampering (registry hygiene) | Published `Core.AspNet` 2.0.0-alpha.0.* on NuGet | low | accept | Unlisting is owner-optional (A1), pre-stable low-stakes; captured in `user_setup`; NOT automated (no publish/unlist executed by the plan). | + + + +Per-task automated verifies run at each task commit (targeted build / grep / restore-assertion). + +Plan-completion gate (run once from `src/`, both TFMs — matches CI): +- `cd src && dotnet restore SimpleSettings.slnx` +- `dotnet build SimpleSettings.slnx -c Release --no-restore -p:ContinuousIntegrationBuild=true` +- `dotnet test SimpleSettings.slnx -c Release --no-build` (full TUnit suite; the internal-flip + package removal are compile/suite-verified — no new test is added by this plan) +- PKG-02 floor assertion: the Task 3 assets check prints `mismatches: []`. + +Note: use TUnit's `--treenode-filter` (Microsoft.Testing.Platform) for any targeted subset — never `dotnet test --filter`, which exits 5 with zero tests. + + + +- SettingsHolder is `internal sealed`; no public-surface reference remains; suite green on net8 + net10 (Phase SC #1). +- `Core.AspNet` is removed from the solution, no project references it, and the solution builds without it (Phase SC #2). +- A net8 restore resolves `Microsoft.Extensions.*` at 8.0.x (not 10.x); net10 resolves 10.0.9 (Phase SC #3). +- Owner-optional NuGet unlist follow-up is documented (not required for phase completion). + + + +Create `.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-SUMMARY.md` when done. + diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-PLAN.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-PLAN.md new file mode 100644 index 0000000..54f23be --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-PLAN.md @@ -0,0 +1,216 @@ +--- +phase: 03-public-surface-packaging-binder-cleanup +plan: 02 +type: execute +wave: 2 +depends_on: ["03-01"] +files_modified: + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs + - src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs +autonomous: true +requirements: [SRC-02] + +must_haves: + truths: + - "A quoted command-line value containing spaces (arriving shell-unquoted as one token, e.g. args {\"--name\",\"John Doe\"}) binds to the full value 'John Doe' (SC #4)." + - "Space-separated `--key value` pairs bind, in addition to the existing inline `--key=value` / `--key:value` (SC #4, D-04)." + - "A prefixed token following a key (`--key --other`) is treated as a new key; the current key stores no value and stays at its default (D-04)." + - "arg[0] (the exe path) is skipped by default and can be re-included by setting SkipFirstArgument=false (SC #4, D-05)." + - "AddCommandLine() feeds properly-tokenized args from Environment.GetCommandLineArgs(), so the quoted-value criterion holds end-to-end for the convenience method (Open Q #1)." + artifacts: + - "CommandLineSettingsBinderOptions.cs — public bool SkipFirstArgument { get; set; } = true;" + - "CommandLineSettingsBinder.cs — Parse rewritten with single-token lookahead + arg[0] skip" + - "SettingsBuilderFactoryExtensions.cs — AddCommandLine using Environment.GetCommandLineArgs()" + - "ArgumentsTests.cs — new TUnit cases for space-separated, quoted-with-spaces, prefixed-next-token, and SkipFirstArgument true/false" + key_links: + - "SkipFirstArgument is read by Parse to choose the iteration start index (1 when true and args are present, else 0)." + - "The lookahead only fires for a token that carried an argument prefix; a following non-prefixed token is consumed as the value, a following prefixed token is left as a new key (preserving the existing 'no value => skip' guard `if (value != null)`)." + - "AddCommandLine -> GetCommandLineArgs() -> AddArguments -> CommandLineSettingsBinder(args, options): a quoted value arrives as a single token instead of being shredded by Split(' ')." +--- + + +Fix the command-line binder to parse real-world arguments (SRC-02). Add space-separated +`--key value` support via single-token lookahead alongside the existing inline `--key=value` / +`--key:value` (D-04); add a `SkipFirstArgument` option (default true) that drops the exe path at +arg[0] (D-05); preserve all existing options (D-06); and switch the `AddCommandLine()` convenience +method to `Environment.GetCommandLineArgs()` so the "quoted value with spaces" criterion holds +end-to-end (Open Q #1, in-scope). + +Purpose: satisfy Phase 3 success criterion #4 — a quoted command-line value containing spaces +binds correctly and arg[0] is skipped. +Output: a lookahead-capable `Parse`, a new `SkipFirstArgument` option, a properly-tokenized +`AddCommandLine`, and TUnit coverage for the new behaviors — all inside the existing +`ISectionBinder` contract (no new abstraction). + +DEPENDS ON 03-01: this plan's verification builds and tests the full solution, which consumes the +finalized package floors (PKG-02) and the Core.AspNet-free graph (PKG-01) that 03-01 establishes. +Run 03-01 first so the restore/build graph is stable. + + + +@/Users/guyludvig/frontegg/development/open-source/SimpleSettings/.claude/gsd-core/workflows/execute-plan.md +@/Users/guyludvig/frontegg/development/open-source/SimpleSettings/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md +@.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md +@.planning/phases/03-public-surface-packaging-binder-cleanup/03-PATTERNS.md +@.planning/codebase/TESTING.md + +# Source of truth for this plan's edits: +@src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs +@src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs +@src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs +@src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs + + + +Symbols/paths this plan creates or changes (read by drift verification to exclude newly-created +symbols): + +- NEW public property: `CommandLineSettingsBinderOptions.SkipFirstArgument` (bool, default `true`) — SRC-02/D-05. +- CHANGED method: `CommandLineSettingsBinder.Parse(string[])` — from a plain foreach to an index/lookahead loop with an arg[0] skip; `SplitByDelimiter` stays as-is. SRC-02/D-04. +- CHANGED method: `SettingsBuilderFactoryExtensions.AddCommandLine` — internals switch from `Environment.CommandLine.Trim().Split(' ')` to `Environment.GetCommandLineArgs()`; signature unchanged. SRC-02/Open Q #1. +- NEW tests in `ArgumentsTests.cs`: space-separated `--k v`, quoted-with-spaces single token, prefixed-next-token = new key, `SkipFirstArgument` true/false. (Executor may instead add a sibling `CommandLineParseTests.cs` in the same dir; keep the class name stable for the treenode-filter.) +- UNCHANGED (do NOT alter): `IsCaseSensitive`, `NameFormatter`, `ArgumentPrefixes` (`-`/`/`), `Delimiters` (`:`/`=`), the fluent `Add*`/`Clear*` mutators, `SetCaseSensitivity` extension, and `AddArguments` (D-06). No manual quote-stripping is added (the shell already unquotes). + + + + + + Task 1: SkipFirstArgument option + lookahead Parse + tests (SRC-02 / D-04, D-05, D-06) + src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs, src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs, src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs + + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs (analog: the existing `IsCaseSensitive` bool auto-prop with default — copy its shape for SkipFirstArgument) + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs (the Parse foreach at lines 37-54 and SplitByDelimiter at 56-90 — Parse changes; SplitByDelimiter stays) + - src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs (existing test shape: `[Test] async Task`, `await Assert.That(...).IsEqualTo(...)`, nested `ICommandLineInterface` with `[SettingsProperty("name")]`, fluent `o => o.SetCaseSensitivity(false)`, the leading-space `Args` fixture) + - .planning/codebase/TESTING.md (TUnit conventions; run via --treenode-filter, never --filter) + - 03-PATTERNS.md §"CommandLineSettingsBinder.cs — Parse / SplitByDelimiter" and §"ArgumentsTests.cs" (the exact 4-step target behavior + Pitfall 4) + - 03-RESEARCH.md §"Pattern 2: CLI single-token lookahead" (SimpleSettings deliberately diverges from Microsoft: a prefixed next-token is a NEW key, not a value) + + + Write these TUnit cases FIRST (RED), then implement Parse + the option to make them pass (GREEN). + Use args arrays (already-tokenized, as a shell/Main would deliver them). A leading dummy token + absorbs the default arg[0] skip unless SkipFirstArgument=false. Match the existing + ICommandLineInterface conventions (`[SettingsProperty("name")] string Name`, `int Age`). + - Inline preserved: args {"x","--name=value"} (default skip drops "x") => Name == "value". + - Inline colon preserved: args {"x","--name:value"} => Name == "value". + - Space-separated (D-04): args {"x","--name","value"} => Name == "value". + - Quoted-with-spaces headline (SC #4): args {"x","--name","John Doe"} => Name == "John Doe" (the space-bearing value arrives as ONE token). + - Prefixed-next = new key (D-04): args {"x","--name","--other"} => Name is NOT set (stays its default); no exception. + - arg[0] kept when off (D-05): args {"--name","value"} with SkipFirstArgument=false => Name == "value"; the SAME array with the default (true) leaves Name unset because "--name" at index 0 is skipped. + - Regression (Pitfall 4): the existing leading-space fixture Args = " name=value age:3" split on space stays green under the new default (arg[0] is the empty string, harmlessly skipped). + + + Per D-05, add a public auto-property SkipFirstArgument (bool, default true) to + CommandLineSettingsBinderOptions, matching the shape of the existing IsCaseSensitive property, with + an XML-doc note that default true suits Environment.CommandLine / GetCommandLineArgs() (which include + the exe at arg[0]) and callers passing Main(string[]) should set it false. + Per D-04, rewrite CommandLineSettingsBinder.Parse from the plain foreach to an index-based (or + enumerator) loop implementing single-token lookahead: (1) if SkipFirstArgument is true and + args.Length > 0, start iteration at index 1; (2) for each token run the existing SplitByDelimiter + (inline `--k=v`/`--k:v` still stored via the existing `if (value != null)` guard, key prefix-trimmed + with `key.TrimStart(_options.ArgumentPrefixes.ToArray())`); (3) when SplitByDelimiter yields a null + value AND the token carried an argument prefix (detect by `_options.ArgumentPrefixes.Contains(token[0])` + or a length compare before/after TrimStart) AND a next token exists that is NOT prefixed, consume the + next token as the value and advance the index; (4) if the next token IS prefixed, leave the current key + valueless (not stored — the next token becomes the new key on the following iteration). Keep + SplitByDelimiter unchanged (min-index delimiter wins, Substring(idx+1) is the value). Per D-06 do NOT + change prefixes/delimiters/IsCaseSensitive/NameFormatter and do NOT add quote-stripping. Do NOT add + switch-mappings or short-flag/boolean semantics (out of scope). Add the new TUnit cases from + to ArgumentsTests.cs (keep the class named ArgumentsTests so the treenode-filter matches); + keep the existing leading-space Args fixture as-is. + + + cd src && dotnet test SimpleSettings.slnx -c Release --treenode-filter "/*/*/ArgumentsTests/*" + + + - CommandLineSettingsBinderOptions.cs contains `public bool SkipFirstArgument { get; set; } = true;`. + - All new cases exist as `[Test] async Task` methods in ArgumentsTests.cs and pass, including one asserting args {"x","--name","John Doe"} binds Name == "John Doe". + - The prefixed-next-token case asserts the current key is NOT stored (Name at default) with no exception thrown. + - The SkipFirstArgument=false case binds arg[0] as a real key; the default (true) case skips it. + - The pre-existing ArgumentsTests methods still pass (leading-space fixture unchanged). + - `dotnet test ... --treenode-filter "/*/*/ArgumentsTests/*"` exits 0 on the local net10 runtime (both TFMs via CI); the `--filter` form is NOT used. + + SkipFirstArgument exists (default true); Parse supports inline + space-separated + quoted-with-spaces + prefixed-next-token = new-key + arg[0] skip; new and existing ArgumentsTests are green. + + + + Task 2: AddCommandLine uses GetCommandLineArgs (SRC-02 / Open Q #1, owner-confirmable in-scope) + src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs + + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs (AddCommandLine at lines 41-49 currently calls `Environment.CommandLine.Trim().Split(' ')`; AddArguments at 51-61 stays unchanged) + - 03-RESEARCH.md §"Open Questions" #1 and §"Pitfall 3" (why Split(' ') shreds quoted values; GetCommandLineArgs returns properly-tokenized string[] with [0] = exe path) + - 03-PATTERNS.md §"SettingsBuilderFactoryExtensions.cs — AddCommandLine" + + + Per Open Q #1 (RESEARCH recommends YES; treat as in-scope this phase — surfaced here as an + owner-confirmable decision, default = apply), replace the current + `Environment.CommandLine.Trim().Split(' ')` tokenization inside AddCommandLine with + `Environment.GetCommandLineArgs()`, which returns a properly-tokenized string[] whose [0] is the exe + path. AddCommandLine keeps passing the resulting args to the existing AddArguments (which builds the + options + CommandLineSettingsBinder), and callers relying on the default SkipFirstArgument=true then + correctly drop the exe path. Do NOT change AddArguments, the method signature, or the return. Do NOT + add quote-stripping (GetCommandLineArgs already tokenizes). This makes the "quoted value with spaces" + criterion hold end-to-end for the convenience method, not just the AddArguments path. + OWNER NOTE: if the owner defers Open Q #1, skip this task and leave AddCommandLine as-is; the D-04 + lookahead fix from Task 1 still satisfies SC #4 for callers passing already-tokenized args + (Main / GetCommandLineArgs). + + + cd src && grep -q 'Environment.GetCommandLineArgs()' Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs && dotnet build SimpleSettings.slnx -c Release + + + - SettingsBuilderFactoryExtensions.cs AddCommandLine sources its args from `Environment.GetCommandLineArgs()`. + - AddArguments and the AddCommandLine signature/return are unchanged. + - The solution builds with 0 errors on both TFMs and the full TUnit suite stays green. + + AddCommandLine feeds properly-tokenized args; the quoted-with-spaces criterion holds end-to-end for the convenience method; suite green. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| OS / shell command line -> library | Untrusted `string[]` argv (and `Environment.GetCommandLineArgs()`) crosses into the binder and becomes string config values. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-03-04 | Tampering | CLI arg parsing of untrusted `string[]` in `CommandLineSettingsBinder.Parse` | low | mitigate | Values are stored as strings only — no eval/command-execution sink; the lookahead adds no injection surface; downstream conversion is already secret-safe (SEC-01, unchanged). | +| T-03-05 | Information Disclosure | `AddCommandLine` reading `Environment.GetCommandLineArgs()` (exe path + args) | low | accept | Args become string config values only; SkipFirstArgument=true drops the exe path by default; no secret handling changes; consumer chooses what to bind. | + + + +Per-task automated verifies run at each task commit (targeted TUnit treenode-filter / grep + build). + +Plan-completion gate (run once from `src/`, both TFMs — matches CI): +- `cd src && dotnet restore SimpleSettings.slnx` +- `dotnet build SimpleSettings.slnx -c Release --no-restore -p:ContinuousIntegrationBuild=true` +- `dotnet test SimpleSettings.slnx -c Release --no-build` (full TUnit suite, including new ArgumentsTests cases) + +TUnit note: targeted runs use `--treenode-filter "/*/*/ArgumentsTests/*"` on Microsoft.Testing.Platform. +The `dotnet test --filter "*..."` form is rejected (exits 5, zero tests). Local runtime is net10; net8 runs in CI. + + + +- A quoted command-line value with spaces binds to the full value; space-separated `--k v` and inline `--k=v`/`--k:v` all bind (Phase SC #4). +- A prefixed token following a key is a new key, not a value; the valueless key is not stored (D-04). +- arg[0] is skipped by default; SkipFirstArgument=false re-includes it (Phase SC #4, D-05). +- AddCommandLine tokenizes via GetCommandLineArgs so the criterion holds end-to-end (Open Q #1). +- Full TUnit suite green on net8 + net10. + + + +Create `.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-SUMMARY.md` when done. + From 3ba5a8c72573c3315007572c42fa345a83f3ed1e Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:00:13 +0300 Subject: [PATCH 07/21] docs(03): refine D-05 (split exe-skip by entry point) + resolve open questions --- .../03-CONTEXT.md | 13 ++++++++++--- .../03-RESEARCH.md | 6 +++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md index 4654db1..2492aee 100644 --- a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md @@ -50,9 +50,16 @@ REL-01 (cut the beta) — Phase 5; COLL-01 (`List` support, still owner-defer is a **new key**, not a value (the current key then has no value → not stored, preserving today's string-only "no value ⇒ skip" semantics). This is the real "quoted value with spaces" fix — such a value arrives shell-unquoted as its own token, which the current parser drops. -- **D-05:** **Skip `arg[0]` (exe path) by default**, exposed as a toggle on - `CommandLineSettingsBinderOptions` (e.g. `SkipFirstArgument`, default `true`) so callers passing - `Main(string[])` args — which already exclude the exe — can turn it off. +- **D-05:** **Skip the exe path at the entry point that actually carries it — not via a shared + default.** `AddCommandLine()` (which reads `Environment.GetCommandLineArgs()`, where `[0]` is the + exe) skips the first token internally; `AddArguments(string[])` binds exactly what it is handed + (no skip — `Main(string[])` args already exclude the exe). A `SkipFirstArgument` toggle on + `CommandLineSettingsBinderOptions` remains for explicit override. + *(Refined 2026-07-14 during Phase-3 planning, owner-approved: the original wording was a single + shared `SkipFirstArgument` default `true`, but architect review showed one shared default silently + drops `args[0]` for `AddArguments(mainArgs)` callers while being correct for `AddCommandLine()`. + Splitting the responsibility by entry point removes that footgun and keeps the intent — skip the + exe only where an exe is present.)* - **D-06:** Preserve existing options (prefixes `-`/`/`, delimiters `:`/`=`, `IsCaseSensitive`, `NameFormatter`). No manual quote-stripping needed (the shell already unquotes). diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md index e0e9389..6a81aa4 100644 --- a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md @@ -304,7 +304,11 @@ public bool SkipFirstArgument { get; set; } = true; **Note:** A1/A2 are the only non-VERIFIED items; both are low-risk registry/graph hygiene, not code correctness. -## Open Questions +## Open Questions (RESOLVED) + +> **Resolved 2026-07-14 (Phase-3 planning, owner-approved):** +> 1. **Q1 — RESOLVED:** `AddCommandLine()` switches to `Environment.GetCommandLineArgs()` (in-scope). The exe-skip is owned by `AddCommandLine()` internally (split-by-entry-point per refined D-05), *not* a shared `SkipFirstArgument` default — so `AddArguments(mainArgs)` binds exactly what it is handed. The "quoted value with spaces" success criterion holds end-to-end for the `AddCommandLine()` path. +> 2. **Q2 — RESOLVED:** net8 floor = **latest-patch-per-package** (Option 1): `Configuration` 8.0.0, `Configuration.Json` 8.0.1, `DependencyInjection` 8.0.1, `DependencyInjection.Abstractions` 8.0.2; net10 stays 10.0.9. 1. **Should `AddCommandLine()` switch from `Environment.CommandLine.Split(' ')` to `Environment.GetCommandLineArgs()`?** - What we know: the current split is quote-unsafe; the success criterion is "a quoted command-line value with spaces binds correctly". The D-04 lookahead fix only helps callers who pass already-tokenized args (`Main`/`GetCommandLineArgs`), **not** the `Environment.CommandLine.Split(' ')` path. From ab4a0925ebe102445a28b34735f7abcb3af1ddef Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:32:54 +0300 Subject: [PATCH 08/21] docs(03): finalize phase 3 plan (2 plans) after plan-check + architect/security/perf review --- .planning/ROADMAP.md | 4 + .planning/STATE.md | 6 +- .../03-01-PLAN.md | 51 +++- .../03-02-PLAN.md | 192 ++++++++----- .../03-PATTERNS.md | 257 ++++++++++++++++++ .../03-RESEARCH.md | 16 +- .../03-VALIDATION.md | 63 +++-- 7 files changed, 468 insertions(+), 121 deletions(-) create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-PATTERNS.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 83c3bbd..aa6ae67 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -77,8 +77,12 @@ value: config → typed settings maps accurately, and never leaks a secret doing 4. A quoted command-line value containing spaces binds correctly and the executable path (`arg[0]`) is skipped. **Plans**: 2 plans +**Wave 1** - [ ] 03-01-PLAN.md — Public surface & packaging cleanup: SettingsHolder → internal sealed (API-01), drop dead Core.AspNet package (PKG-01), per-TFM Microsoft.Extensions.* floor (PKG-02) [Wave 1] + +**Wave 2** *(blocked on Wave 1 completion)* + - [ ] 03-02-PLAN.md — Command-line binder cleanup: SkipFirstArgument option + space-separated `--k v` lookahead + arg[0] skip + AddCommandLine tokenization (SRC-02) [Wave 2, depends on 03-01] ### Phase 4: AOT/Trim Honesty & Documentation diff --git a/.planning/STATE.md b/.planning/STATE.md index 68510ee..3295669 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,9 +4,9 @@ milestone: v2.0.0 milestone_name: milestone current_phase: 3 current_phase_name: Public Surface, Packaging & Binder Cleanup -status: verifying +status: executing stopped_at: Phase 3 context gathered -last_updated: "2026-07-14T10:59:02.042Z" +last_updated: "2026-07-14T12:32:26.326Z" last_activity: 2026-07-14 last_activity_desc: Phase 02 complete, transitioned to Phase 3 progress: @@ -30,7 +30,7 @@ See: .planning/PROJECT.md (updated 2026-07-13) Phase: 3 — Public Surface, Packaging & Binder Cleanup Plan: Not started -Status: Phase complete — ready for verification +Status: Ready to execute Last activity: 2026-07-14 — Phase 02 complete, transitioned to Phase 3 Progress: [██░░░░░░░░] 20% diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-PLAN.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-PLAN.md index e599ac8..22f64be 100644 --- a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-PLAN.md +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-PLAN.md @@ -23,7 +23,7 @@ must_haves: truths: - "SettingsHolder no longer appears on the public API surface — it compiles as internal sealed, reachable from tests only via InternalsVisibleTo (SC #1)." - "Core.AspNet is absent from the solution and no project references it; the solution builds without it (SC #2)." - - "A net8 build resolves Microsoft.Extensions.* at 8.0.x (not 10.x); a net10 build resolves 10.0.9 (SC #3)." + - "The two PACKABLE consumer floors resolve per-TFM: Microsoft.Extensions.Configuration (in the Binders package) and Microsoft.Extensions.DependencyInjection.Abstractions (in the GenericHost package) resolve to 8.0.x on net8 (NOT 10.x) and 10.0.9 on net10 — verified against those projects' project.assets.json, with a guard failing on zero microsoft.extensions.* entries (SC #3)." - "The full test suite stays green on net8 and net10 after all three edits (SC #1)." artifacts: - "src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs — internal sealed class SettingsHolder" @@ -32,7 +32,7 @@ must_haves: - "src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj — no Core.AspNet ProjectReference" key_links: - "Info.cs InternalsVisibleTo(UnitTests, Benchmark) keeps the internal-flipped SettingsHolder reachable from the test + benchmark assemblies." - - "Directory.Packages.props $(TargetFramework) conditions are evaluated per-TFM by NuGet restore → the net8.0 target in project.assets.json lists 8.0.x, the net10.0 target lists 10.0.9." + - "Directory.Packages.props $(TargetFramework) conditions are evaluated per-TFM by NuGet restore → the net8.0 target lists 8.0.x, the net10.0 target lists 10.0.9 in the assets of the projects that actually reference the packages (Binders for Configuration, GenericHost for DependencyInjection.Abstractions) — NOT the core lib, which references none." - "The test project's local duplicate Core/AspNet/Environments.cs (namespace ...UnitTests.Core.AspNet) is independent of the removed package and keeps the Core/AspNet tests compiling." --- @@ -76,7 +76,7 @@ newly-created or intentionally-removed surface): - CHANGED symbol: `ExistForAll.SimpleSettings.SettingsHolder` — accessibility `public class` -> `internal sealed class` (removed from public API surface; API-01/D-01). - REMOVED project/package: `ExistForAll.SimpleSettings.Core.AspNet` (directory `src/Core/ExistForAll.SimpleSettings.Core.AspNet/`, its `.csproj`, and its `Environments.cs`); removed from `src/SimpleSettings.slnx` and from `UnitTests.csproj` ProjectReference (PKG-01/D-02). -- NEW build items: per-TFM conditional `PackageVersion` items in `src/Directory.Packages.props` — two mutually-exclusive items (net8.0 / net10.0) per package for the four `Microsoft.Extensions.*` packages (PKG-02/D-03). Net8 floors: Configuration `8.0.0`, Configuration.Json `8.0.1`, DependencyInjection `8.0.1`, DependencyInjection.Abstractions `8.0.2`; net10 stays `10.0.9`. +- NEW build items: per-TFM conditional `PackageVersion` items in `src/Directory.Packages.props` — two mutually-exclusive items (net8.0 / net10.0) per package for the four `Microsoft.Extensions.*` packages (PKG-02/D-03). Net8 floors: Configuration `8.0.0`, Configuration.Json `8.0.1`, DependencyInjection `8.0.1`, DependencyInjection.Abstractions `8.0.2`; net10 stays `10.0.9`. Consumer-facing scope: only `Configuration` (direct ref of the packable Binders package) and `DependencyInjection.Abstractions` (direct ref of the packable GenericHost package) are floors that ship to consumers; `Configuration.Json` + `DependencyInjection` are referenced only by the non-packable UnitTests/Benchmark projects and never ship (their items still change to keep the local/CI build consistent). - UNCHANGED (kept, do NOT touch): the test project's local duplicate `src/Tests/ExistForAll.SimpleSettings.UnitTests/Core/AspNet/Environments.cs` (namespace `...UnitTests.Core.AspNet`); TUnit/BenchmarkDotNet/MinVer `PackageVersion` items; `CentralPackageTransitivePinningEnabled` stays unset (off). @@ -173,17 +173,53 @@ newly-created or intentionally-removed surface): Do NOT add an unconditional item alongside a conditional one for the same package (that trips NU1504). Do NOT set CentralPackageTransitivePinningEnabled (stays off — correct, only direct deps are pinned). Leave the TUnit / BenchmarkDotNet / MinVer ItemGroups untouched. + SCOPE (which floors reach consumers): only two of the four are consumer-facing floors. The core + library `ExistForAll.SimpleSettings.csproj` has NO Microsoft.Extensions.* PackageReference, so its + assets carry none — asserting against it is vacuous. `Microsoft.Extensions.Configuration` is a direct + PackageReference of the PACKABLE `ExistForAll.SimpleSettings.Binders` project (dir + `Core/ExistForAll.SimpleSettings.Extensions.Binders/`), and + `Microsoft.Extensions.DependencyInjection.Abstractions` is a direct PackageReference of the PACKABLE + `ExistForAll.SimpleSettings.Extensions.GenericHost` project — these two are what "free net8 consumers". + `Microsoft.Extensions.Configuration.Json` and `Microsoft.Extensions.DependencyInjection` are referenced + ONLY by the NON-packable UnitTests / Benchmark projects and never ship (still float per-TFM for a + consistent local/CI build, but they are not a shipped consumer floor). All four PackageVersion items + still change; the per-TFM resolution is verified against the two packable projects' assets (not the + core lib), with a guard that fails on zero microsoft.extensions.* entries so a vacuous pass cannot recur. NOTE (owner-resolvable, already decided): net8 floor = latest-patch-per-package (Option 1) per the decision wording; a uniform 8.0.0 for all four (Option 2, friendliest-lowest floor) is also valid. Ship Option 1 unless the owner prefers uniform 8.0.0. - cd src && dotnet restore SimpleSettings.slnx && dotnet build SimpleSettings.slnx -c Release --no-restore -p:TreatWarningsAsErrors=true && python3 -c 'import json,glob,sys; t=json.load(open(glob.glob("Core/ExistForAll.SimpleSettings/obj/project.assets.json")[0]))["targets"]; bad=[(f,k) for f,libs in t.items() for k in libs if k.lower().startswith("microsoft.extensions.") and ((f.startswith("net8") and not k.split("/")[1].startswith("8.")) or (f.startswith("net10") and not k.split("/")[1].startswith("10.")))]; print("mismatches:",bad); sys.exit(1 if bad else 0)' + cd src && LOG=$(mktemp) && dotnet restore SimpleSettings.slnx -p:NuGetAudit=true -p:NuGetAuditMode=all > "$LOG" 2>&1 && cat "$LOG" && ! grep -Eq 'NU190[1-4]' "$LOG" && dotnet build SimpleSettings.slnx -c Release --no-restore -p:TreatWarningsAsErrors=true && python3 -c 'import json,glob,sys +checks=[("Core/ExistForAll.SimpleSettings.Extensions.Binders","Microsoft.Extensions.Configuration"),("Core/ExistForAll.SimpleSettings.Extensions.GenericHost","Microsoft.Extensions.DependencyInjection.Abstractions")] +errs=[] +for proj,pkg in checks: + fs=glob.glob(proj+"/obj/project.assets.json") + if not fs: + errs.append((proj,"no-assets")); continue + t=json.load(open(fs[0]))["targets"] + for tfm in ("net8.0","net10.0"): + libs=t.get(tfm) or {} + me={k.split("/")[0]:k.split("/")[1] for k in libs if k.lower().startswith("microsoft.extensions.")} + if not me: + errs.append((proj,tfm,"GUARD-zero-ms-ext")); continue + want="8." if tfm=="net8.0" else "10." + if pkg not in me: + errs.append((proj,tfm,"missing-"+pkg)); continue + if not me[pkg].startswith(want): + errs.append((proj,tfm,pkg+"="+me[pkg])) + leak=[k+"="+v for k,v in me.items() if not v.startswith(want)] + if leak: + errs.append((proj,tfm,"floor-leak:"+";".join(leak))) +print("floor-check-errors:",errs) +sys.exit(1 if errs else 0)' - `Directory.Packages.props` has eight conditional `Microsoft.Extensions.*` items (two per package), each carrying a mutually-exclusive `$(TargetFramework)` Condition; no unconditional `Microsoft.Extensions.*` item remains. - `dotnet build -p:TreatWarningsAsErrors=true` succeeds with 0 warnings (no NU1504 duplicate, no NU1605/NU1608 downgrade) on both TFMs. - - The assets check prints `mismatches: []` and exits 0 — the net8.0 target resolves `Microsoft.Extensions.*` at 8.0.x and the net10.0 target at 10.0.9. + - `dotnet restore` with NuGet audit emits NO `NU1901`–`NU1904` advisory (the verify greps the restore log and fails on any match) — a vulnerable transitive floor is a hard gate, not a swallowed restore-only warning. + - The floor check inspects the assets of the two PACKABLE consumer projects — `Core/ExistForAll.SimpleSettings.Extensions.Binders/obj/project.assets.json` (asserts `Microsoft.Extensions.Configuration` → net8 `8.0.x`, NOT `10.`; net10 `10.0.9`) and `Core/ExistForAll.SimpleSettings.Extensions.GenericHost/obj/project.assets.json` (asserts `Microsoft.Extensions.DependencyInjection.Abstractions` → net8 `8.0.x`, NOT `10.`; net10 `10.0.9`) — and prints `floor-check-errors: []` exiting 0. + - The floor check FAILS (`GUARD-zero-ms-ext`) if a TFM has zero `microsoft.extensions.*` entries, so pointing at a project with no such reference (e.g. the core lib) can never produce a false green. net8 consumers float to 8.0.x, net10 stays 10.0.9; restore + warnings-as-errors build clean on both TFMs; project.assets.json confirms per-TFM resolution. @@ -203,7 +239,7 @@ newly-created or intentionally-removed surface): | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| | T-03-01 | Information Disclosure | `SettingsHolder` public surface | low | mitigate | API-01/D-01 makes it `internal sealed`, removing an internal DTO from the public surface (net surface-reduction; no new exposure). | -| T-03-02 | Tampering (supply chain) | `Microsoft.Extensions.*` net8 floor float (PKG-02) | low | mitigate | First-party packages only; net8 floors 8.0.0/8.0.1/8.0.1/8.0.2 verified against nuget.org flatcontainer (2026-07-14); no known critical advisory for these versions; consumers may still float up. CPM conditional items; `CentralPackageTransitivePinningEnabled` stays off; warnings-as-errors build catches NU1504/NU1605. | +| T-03-02 | Tampering (supply chain) | `Microsoft.Extensions.*` net8 floor float (PKG-02) | low | mitigate | First-party packages only; net8 floors 8.0.0/8.0.1/8.0.1/8.0.2 verified against nuget.org flatcontainer (2026-07-14). No known critical advisory for the **direct, shipped** floors (Configuration in the Binders package, DependencyInjection.Abstractions in the GenericHost package); consumers may still float up. A restore-time NuGet audit gate (Task 3 verify: `NuGetAuditMode=all`, fail on any `NU1901`–`NU1904`) catches a vulnerable direct-or-transitive floor at restore (where those advisories fire), not just at build. CPM conditional items; `CentralPackageTransitivePinningEnabled` stays off; warnings-as-errors build catches NU1504/NU1605. | | T-03-03 | Tampering (registry hygiene) | Published `Core.AspNet` 2.0.0-alpha.0.* on NuGet | low | accept | Unlisting is owner-optional (A1), pre-stable low-stakes; captured in `user_setup`; NOT automated (no publish/unlist executed by the plan). | @@ -214,7 +250,8 @@ Plan-completion gate (run once from `src/`, both TFMs — matches CI): - `cd src && dotnet restore SimpleSettings.slnx` - `dotnet build SimpleSettings.slnx -c Release --no-restore -p:ContinuousIntegrationBuild=true` - `dotnet test SimpleSettings.slnx -c Release --no-build` (full TUnit suite; the internal-flip + package removal are compile/suite-verified — no new test is added by this plan) -- PKG-02 floor assertion: the Task 3 assets check prints `mismatches: []`. +- PKG-02 restore audit: `dotnet restore` with `NuGetAuditMode=all` emits no `NU1901`–`NU1904` (Task 3 verify greps the restore log and fails on any advisory). +- PKG-02 floor assertion: the Task 3 floor check prints `floor-check-errors: []` against the Binders + GenericHost assets (the packable projects that actually reference the packages), with a guard that fails on zero microsoft.extensions.* entries. Note: use TUnit's `--treenode-filter` (Microsoft.Testing.Platform) for any targeted subset — never `dotnet test --filter`, which exits 5 with zero tests. diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-PLAN.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-PLAN.md index 54f23be..2ac8249 100644 --- a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-PLAN.md +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-PLAN.md @@ -16,33 +16,41 @@ must_haves: truths: - "A quoted command-line value containing spaces (arriving shell-unquoted as one token, e.g. args {\"--name\",\"John Doe\"}) binds to the full value 'John Doe' (SC #4)." - "Space-separated `--key value` pairs bind, in addition to the existing inline `--key=value` / `--key:value` (SC #4, D-04)." - - "A prefixed token following a key (`--key --other`) is treated as a new key; the current key stores no value and stays at its default (D-04)." - - "arg[0] (the exe path) is skipped by default and can be re-included by setting SkipFirstArgument=false (SC #4, D-05)." - - "AddCommandLine() feeds properly-tokenized args from Environment.GetCommandLineArgs(), so the quoted-value criterion holds end-to-end for the convenience method (Open Q #1)." + - "A prefixed token following a key (`--key --other`) is treated as a new key; the current key stores no value and stays at its default (D-04). This also means a space-separated VALUE that itself starts with a prefix (`--name /etc/hosts`) does NOT bind — that value must use the inline form (`--name=/etc/hosts`); intended, documented on the option (D-04/D-06, A4)." + - "The exe path is skipped ONLY at the AddCommandLine() entry point (which sources Environment.GetCommandLineArgs(), where [0] is the exe); AddArguments(string[]) binds exactly what it is handed with NO default skip, because Main(string[]) args already exclude the exe. SkipFirstArgument (default false) is the explicit per-binder override (SC #4, D-05 refined)." + - "An empty-string token is parsed safely (no IndexOutOfRangeException) and is not stored (A3)." + - "AddCommandLine() feeds properly-tokenized args from Environment.GetCommandLineArgs() and enables the exe skip internally, so the quoted-value criterion holds end-to-end for the convenience method UNCONDITIONALLY (Open Q #1 RESOLVED, in-scope)." + - "Binding an unconvertible CLI value never leaks that value: the thrown exception's full ToString() chain excludes it (SEC-01 / S1 discharged for the CLI path)." artifacts: - - "CommandLineSettingsBinderOptions.cs — public bool SkipFirstArgument { get; set; } = true;" - - "CommandLineSettingsBinder.cs — Parse rewritten with single-token lookahead + arg[0] skip" - - "SettingsBuilderFactoryExtensions.cs — AddCommandLine using Environment.GetCommandLineArgs()" - - "ArgumentsTests.cs — new TUnit cases for space-separated, quoted-with-spaces, prefixed-next-token, and SkipFirstArgument true/false" + - "CommandLineSettingsBinderOptions.cs — public bool SkipFirstArgument { get; set; } = false; (explicit override; default does NOT drop args[0] for AddArguments callers)" + - "CommandLineSettingsBinder.cs — Parse rewritten with single-token lookahead + optional index-0 skip, empty-safe zero-alloc prefix detection (length-delta after TrimStart / Array.IndexOf on a cached prefix array; NOT bare token[0])" + - "SettingsBuilderFactoryExtensions.cs — AddCommandLine using Environment.GetCommandLineArgs() and enabling SkipFirstArgument=true internally (exe skip owned by this entry point)" + - "ArgumentsTests.cs — new TUnit cases: space-separated, quoted-with-spaces, prefixed-next-token=new-key, AddArguments binds arg[0] (no drop), explicit SkipFirstArgument true/false, empty-token no-crash, prefixed-value non-binding (A4), and a CLI-path secret-redaction regression (S2)" key_links: - - "SkipFirstArgument is read by Parse to choose the iteration start index (1 when true and args are present, else 0)." - - "The lookahead only fires for a token that carried an argument prefix; a following non-prefixed token is consumed as the value, a following prefixed token is left as a new key (preserving the existing 'no value => skip' guard `if (value != null)`)." - - "AddCommandLine -> GetCommandLineArgs() -> AddArguments -> CommandLineSettingsBinder(args, options): a quoted value arrives as a single token instead of being shredded by Split(' ')." + - "SkipFirstArgument (default false) is read by Parse to choose the iteration start index (1 when true and args are present, else 0). AddArguments leaves it false (binds exactly); AddCommandLine sets it true so the exe token at [0] is dropped." + - "The lookahead only fires for a token that carried an argument prefix (detected by name.Length != key.Length after TrimStart); a following non-prefixed token is consumed as the value, a following prefixed token is left as a new key (preserving the existing 'no value => skip' guard `if (value != null)`)." + - "AddCommandLine -> GetCommandLineArgs() -> AddArguments(args, o => { o.SkipFirstArgument = true; action?(o); }) -> CommandLineSettingsBinder(args, options): a quoted value arrives as a single token instead of being shredded by Split(' '), and the exe at [0] is dropped by the entry-point-set skip." + - "The rewritten Parse runs at construction/config-time, OUTSIDE the ValuesPopulator try/catch that chains a binder throw into SettingsBindingException — the redaction-critical BindPropertySettings stays store-only and untouched (S1)." --- Fix the command-line binder to parse real-world arguments (SRC-02). Add space-separated `--key value` support via single-token lookahead alongside the existing inline `--key=value` / -`--key:value` (D-04); add a `SkipFirstArgument` option (default true) that drops the exe path at -arg[0] (D-05); preserve all existing options (D-06); and switch the `AddCommandLine()` convenience -method to `Environment.GetCommandLineArgs()` so the "quoted value with spaces" criterion holds -end-to-end (Open Q #1, in-scope). +`--key:value` (D-04); split the exe-skip by entry point (D-05 refined, owner-approved) — a +`SkipFirstArgument` toggle on the options defaults to **false** (AddArguments binds exactly what it +is handed, since Main(string[]) excludes the exe) while `AddCommandLine()` sources +`Environment.GetCommandLineArgs()` and enables the skip internally to drop the exe at arg[0]; +preserve all existing options (D-06); and make `AddCommandLine()` tokenize via +`Environment.GetCommandLineArgs()` so the "quoted value with spaces" criterion holds end-to-end +(Open Q #1 RESOLVED — CONFIRMED in-scope this phase, not deferrable). Purpose: satisfy Phase 3 success criterion #4 — a quoted command-line value containing spaces -binds correctly and arg[0] is skipped. -Output: a lookahead-capable `Parse`, a new `SkipFirstArgument` option, a properly-tokenized -`AddCommandLine`, and TUnit coverage for the new behaviors — all inside the existing -`ISectionBinder` contract (no new abstraction). +binds correctly (UNCONDITIONALLY, via `AddCommandLine()`) and the exe is skipped only where it is +actually present, without silently dropping a real first argument for `AddArguments` callers. +Output: a lookahead-capable `Parse` with empty-safe zero-alloc prefix detection, a `SkipFirstArgument` +option (default false, explicit override), a properly-tokenized exe-skipping `AddCommandLine`, and +TUnit coverage for the new behaviors incl. a CLI-path secret-redaction regression — all inside the +existing `ISectionBinder` contract (no new abstraction). DEPENDS ON 03-01: this plan's verification builds and tests the full solution, which consumes the finalized package floors (PKG-02) and the Core.AspNet-free graph (PKG-01) that 03-01 establishes. @@ -74,11 +82,12 @@ Run 03-01 first so the restore/build graph is stable. Symbols/paths this plan creates or changes (read by drift verification to exclude newly-created symbols): -- NEW public property: `CommandLineSettingsBinderOptions.SkipFirstArgument` (bool, default `true`) — SRC-02/D-05. -- CHANGED method: `CommandLineSettingsBinder.Parse(string[])` — from a plain foreach to an index/lookahead loop with an arg[0] skip; `SplitByDelimiter` stays as-is. SRC-02/D-04. -- CHANGED method: `SettingsBuilderFactoryExtensions.AddCommandLine` — internals switch from `Environment.CommandLine.Trim().Split(' ')` to `Environment.GetCommandLineArgs()`; signature unchanged. SRC-02/Open Q #1. -- NEW tests in `ArgumentsTests.cs`: space-separated `--k v`, quoted-with-spaces single token, prefixed-next-token = new key, `SkipFirstArgument` true/false. (Executor may instead add a sibling `CommandLineParseTests.cs` in the same dir; keep the class name stable for the treenode-filter.) -- UNCHANGED (do NOT alter): `IsCaseSensitive`, `NameFormatter`, `ArgumentPrefixes` (`-`/`/`), `Delimiters` (`:`/`=`), the fluent `Add*`/`Clear*` mutators, `SetCaseSensitivity` extension, and `AddArguments` (D-06). No manual quote-stripping is added (the shell already unquotes). +- NEW public property: `CommandLineSettingsBinderOptions.SkipFirstArgument` (bool, default `false`) — SRC-02/D-05 refined. Default false so `AddArguments` binds exactly what it is handed; it is the explicit per-binder override. +- CHANGED method: `CommandLineSettingsBinder.Parse(string[])` — from a plain foreach to an index/lookahead loop with an optional index-0 skip (driven by `SkipFirstArgument`); empty-safe zero-alloc prefix detection (cache the prefix `char[]` once; current-token "was prefixed" = `name.Length != key.Length`; next-token prefixed = `next.Length > 0 && Array.IndexOf(prefixes, next[0]) >= 0`) — NOT the bare `token[0]` / `ArgumentPrefixes.Contains(token[0])` form. `SplitByDelimiter` stays as-is. SRC-02/D-04, A3, P1. +- CHANGED method: `SettingsBuilderFactoryExtensions.AddCommandLine` — internals switch from `Environment.CommandLine.Trim().Split(' ')` to `Environment.GetCommandLineArgs()`, and it enables `SkipFirstArgument=true` internally (exe skip owned by this entry point) while still honoring a caller override; signature unchanged. SRC-02/Open Q #1 (CONFIRMED in-scope). +- CHANGED (doc only): a one-line XML-doc note on `CommandLineSettingsBinderOptions.ArgumentPrefixes` documenting that a space-separated value beginning with a prefix char (`-`/`/`) is treated as a new key and does not bind — use the inline `--key=value` form for such values (A4/D-06). +- NEW tests in `ArgumentsTests.cs` (keep the class name `ArgumentsTests` stable for the treenode-filter): space-separated `--k v`, quoted-with-spaces single token, prefixed-next-token = new key, `AddArguments` binds arg[0] (no drop), explicit `SkipFirstArgument` true/false, empty-token no-crash (A3), prefixed-value non-binding (A4), and a CLI-path secret-redaction regression (S2). +- UNCHANGED (do NOT alter): `IsCaseSensitive`, `NameFormatter`, `ArgumentPrefixes` (`-`/`/`) semantics, `Delimiters` (`:`/`=`), the fluent `Add*`/`Clear*` mutators, `SetCaseSensitivity` extension, `AddArguments` (D-06), and the redaction-critical `BindPropertySettings` (stays store-only; S1). No manual quote-stripping is added (the shell already unquotes). @@ -96,82 +105,112 @@ symbols): Write these TUnit cases FIRST (RED), then implement Parse + the option to make them pass (GREEN). - Use args arrays (already-tokenized, as a shell/Main would deliver them). A leading dummy token - absorbs the default arg[0] skip unless SkipFirstArgument=false. Match the existing - ICommandLineInterface conventions (`[SettingsProperty("name")] string Name`, `int Age`). - - Inline preserved: args {"x","--name=value"} (default skip drops "x") => Name == "value". - - Inline colon preserved: args {"x","--name:value"} => Name == "value". - - Space-separated (D-04): args {"x","--name","value"} => Name == "value". - - Quoted-with-spaces headline (SC #4): args {"x","--name","John Doe"} => Name == "John Doe" (the space-bearing value arrives as ONE token). - - Prefixed-next = new key (D-04): args {"x","--name","--other"} => Name is NOT set (stays its default); no exception. - - arg[0] kept when off (D-05): args {"--name","value"} with SkipFirstArgument=false => Name == "value"; the SAME array with the default (true) leaves Name unset because "--name" at index 0 is skipped. - - Regression (Pitfall 4): the existing leading-space fixture Args = " name=value age:3" split on space stays green under the new default (arg[0] is the empty string, harmlessly skipped). + Use args arrays (already-tokenized, as a shell/Main would deliver them). SkipFirstArgument now + defaults to FALSE (D-05 refined), so AddArguments binds every token including index 0 — no dummy + leading token is needed. Match the existing ICommandLineInterface conventions + (`[SettingsProperty("name")] string Name`, `int Age`). + - Inline preserved: args {"--name=value"} => Name == "value" (index-0 token binds — proves no default drop, A2 test a). + - Inline colon preserved: args {"--name:value"} => Name == "value". + - Space-separated (D-04): args {"--name","value"} => Name == "value". + - Quoted-with-spaces headline (SC #4): args {"--name","John Doe"} => Name == "John Doe" (the space-bearing value arrives as ONE token). + - Prefixed-next = new key (D-04): args {"--name","--other"} => Name is NOT set (stays its default); no exception. + - Explicit skip override (D-05, A2 test c): args {"--name","value"} with SkipFirstArgument=true => index-0 "--name" is skipped, leaving bare "value" (not stored) => Name NOT set; the SAME array with the default (false) => Name == "value". + - Empty-token no-crash (A3/P1): args {"","--name","value"} (default false, or built via " --name value".Split(' ')) => Name == "value" with NO IndexOutOfRangeException; the empty token is parsed safely and not stored. + - Prefixed-value non-binding (A4/D-06): args {"--name","/etc/hosts"} => Name is NOT set — the value starts with the "/" prefix so it is treated as a new key (intended; use inline --name=/etc/hosts to bind such a value). + - CLI-path secret redaction (S2/SEC-01): bind an unconvertible secret to the int property via args {"--Age="} (SkipFirstArgument=false); GetSettings throws, and the thrown exception's full ToString() chain EXCLUDES the sentinel (while still naming Age + Int32). + - Regression (Pitfall 4): the existing leading-space fixture Args = " name=value age:3" split on space stays green under the new default false — arg[0] is the empty string, which is parsed safely and not stored (name/age bind from [1]/[2]). - Per D-05, add a public auto-property SkipFirstArgument (bool, default true) to - CommandLineSettingsBinderOptions, matching the shape of the existing IsCaseSensitive property, with - an XML-doc note that default true suits Environment.CommandLine / GetCommandLineArgs() (which include - the exe at arg[0]) and callers passing Main(string[]) should set it false. - Per D-04, rewrite CommandLineSettingsBinder.Parse from the plain foreach to an index-based (or - enumerator) loop implementing single-token lookahead: (1) if SkipFirstArgument is true and - args.Length > 0, start iteration at index 1; (2) for each token run the existing SplitByDelimiter - (inline `--k=v`/`--k:v` still stored via the existing `if (value != null)` guard, key prefix-trimmed - with `key.TrimStart(_options.ArgumentPrefixes.ToArray())`); (3) when SplitByDelimiter yields a null - value AND the token carried an argument prefix (detect by `_options.ArgumentPrefixes.Contains(token[0])` - or a length compare before/after TrimStart) AND a next token exists that is NOT prefixed, consume the - next token as the value and advance the index; (4) if the next token IS prefixed, leave the current key - valueless (not stored — the next token becomes the new key on the following iteration). Keep - SplitByDelimiter unchanged (min-index delimiter wins, Substring(idx+1) is the value). Per D-06 do NOT - change prefixes/delimiters/IsCaseSensitive/NameFormatter and do NOT add quote-stripping. Do NOT add - switch-mappings or short-flag/boolean semantics (out of scope). Add the new TUnit cases from - to ArgumentsTests.cs (keep the class named ArgumentsTests so the treenode-filter matches); - keep the existing leading-space Args fixture as-is. + Per D-05 (refined, owner-approved), add a public auto-property SkipFirstArgument (bool, default + FALSE) to CommandLineSettingsBinderOptions, matching the shape of the existing IsCaseSensitive + property. XML-doc it as the explicit per-binder override for dropping the first token: default false + means AddArguments binds exactly what it is handed (Main(string[]) already excludes the exe), and + AddCommandLine() sets it true internally because Environment.GetCommandLineArgs() carries the exe at + index 0. The default must NOT silently drop args[0] for AddArguments callers. + Per A4/D-06, add a one-line XML-doc note to the existing ArgumentPrefixes property stating that a + space-separated value beginning with a prefix char is treated as a new key and does not bind — such a + value must use the inline delimiter form (e.g. an `=`-joined key/value). Describe this by concept; do + not paste a sample that would echo a prefixed literal into the file. + Per D-04, rewrite CommandLineSettingsBinder.Parse from the plain foreach to an index-based loop + implementing single-token lookahead. Before the loop, cache the prefix set once as a local char[] + (from ArgumentPrefixes) — do NOT call ToArray() per token. Then: (1) if SkipFirstArgument is true and + args.Length is greater than 0, start iteration at index 1, else at 0; (2) for each token run the + existing SplitByDelimiter (inline `--k=v`/`--k:v` still stored via the existing `if (value != null)` + guard, key prefix-trimmed via TrimStart over the cached prefix array to compute `name`); (3) determine + whether the current token carried a prefix by the length delta `name.Length != key.Length` (both + already computed) — this is empty-safe and allocation-free; (4) when SplitByDelimiter yields a null + value AND the current token was prefixed AND a next token exists whose first char is NOT a prefix, + consume the next token as the value and advance the index; (5) if the next token IS prefixed, leave the + current key valueless (not stored — the next token becomes the new key on the following iteration). + Detect "next token is prefixed" with an empty-safe, zero-alloc test — `next.Length > 0 && + Array.IndexOf(prefixes, next[0]) >= 0` over the cached array. MANDATORY: do NOT use the bare + `token[0]` index (throws IndexOutOfRangeException on an empty-string token — reachable now that the + default no longer skips index 0) and do NOT use `ArgumentPrefixes.Contains(token[0])` (boxes the + enumerator per token). Keep SplitByDelimiter unchanged (min-index delimiter wins, Substring(idx+1) is + the value). Per D-06 do NOT change prefixes/delimiters/IsCaseSensitive/NameFormatter and do NOT add + quote-stripping. Do NOT add switch-mappings or short-flag/boolean semantics (out of scope). Do NOT + touch BindPropertySettings (stays store-only; S1). Add the new TUnit cases from to + ArgumentsTests.cs (keep the class named ArgumentsTests so the treenode-filter matches), including the + empty-token no-crash case (A3), the prefixed-value non-binding case (A4), and the CLI-path + secret-redaction regression (S2 — bind an unconvertible sentinel to the int Age via an inline + `--Age=`-joined arg with SkipFirstArgument=false, catch the thrown exception, and assert its full + ToString() chain does not contain the sentinel while still naming Age + Int32). Keep the existing + leading-space Args fixture as-is. cd src && dotnet test SimpleSettings.slnx -c Release --treenode-filter "/*/*/ArgumentsTests/*" - - CommandLineSettingsBinderOptions.cs contains `public bool SkipFirstArgument { get; set; } = true;`. - - All new cases exist as `[Test] async Task` methods in ArgumentsTests.cs and pass, including one asserting args {"x","--name","John Doe"} binds Name == "John Doe". + - CommandLineSettingsBinderOptions.cs contains `public bool SkipFirstArgument { get; set; } = false;` (default false — no silent arg[0] drop for AddArguments callers). + - ArgumentPrefixes carries a one-line XML-doc note that a space-separated value starting with a prefix char is a new key, not a value (A4). + - All new cases exist as `[Test] async Task` methods in ArgumentsTests.cs and pass, including one asserting args {"--name","John Doe"} binds Name == "John Doe". - The prefixed-next-token case asserts the current key is NOT stored (Name at default) with no exception thrown. - - The SkipFirstArgument=false case binds arg[0] as a real key; the default (true) case skips it. + - With the default (false), args {"--name","value"} binds arg[0] as a real key (Name == "value"); with SkipFirstArgument=true the same array leaves Name unset (index-0 token skipped). + - The empty-token case (args {"","--name","value"}, default false) binds Name == "value" and throws NO IndexOutOfRangeException (A3). + - The prefixed-value case (args {"--name","/etc/hosts"}) leaves Name unset (A4). + - The CLI-path redaction case throws on the unconvertible sentinel and its exception ToString() chain excludes the sentinel while naming Age + Int32 (S2). + - Parse uses no per-token allocation for prefix detection (cached prefix array; `name.Length != key.Length` and `Array.IndexOf`) and no bare `token[0]` indexing (P1/A3). - The pre-existing ArgumentsTests methods still pass (leading-space fixture unchanged). - `dotnet test ... --treenode-filter "/*/*/ArgumentsTests/*"` exits 0 on the local net10 runtime (both TFMs via CI); the `--filter` form is NOT used. - SkipFirstArgument exists (default true); Parse supports inline + space-separated + quoted-with-spaces + prefixed-next-token = new-key + arg[0] skip; new and existing ArgumentsTests are green. + SkipFirstArgument exists (default false, explicit override); Parse supports inline + space-separated + quoted-with-spaces + prefixed-next-token = new-key, is empty-safe and zero-alloc in prefix detection, and skips index 0 only when the option is true; the CLI-path redaction regression, empty-token, and prefixed-value cases all pass; new and existing ArgumentsTests are green. - Task 2: AddCommandLine uses GetCommandLineArgs (SRC-02 / Open Q #1, owner-confirmable in-scope) + Task 2: AddCommandLine uses GetCommandLineArgs + owns the exe skip (SRC-02 / Open Q #1 RESOLVED, in-scope) src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs - - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs (AddCommandLine at lines 41-49 currently calls `Environment.CommandLine.Trim().Split(' ')`; AddArguments at 51-61 stays unchanged) - - 03-RESEARCH.md §"Open Questions" #1 and §"Pitfall 3" (why Split(' ') shreds quoted values; GetCommandLineArgs returns properly-tokenized string[] with [0] = exe path) + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs (AddCommandLine at lines 41-49 currently calls `Environment.CommandLine.Trim().Split(' ')`; AddArguments at 51-61 stays unchanged and builds the options + CommandLineSettingsBinder from the caller's action) + - 03-RESEARCH.md §"Open Questions" (RESOLVED) #1 and §"Pitfall 3" (why Split(' ') shreds quoted values; GetCommandLineArgs returns properly-tokenized string[] with [0] = exe path) - 03-PATTERNS.md §"SettingsBuilderFactoryExtensions.cs — AddCommandLine" + - 03-CONTEXT.md D-05 (refined) — the exe skip is owned by the entry point that carries the exe, not a shared default - Per Open Q #1 (RESEARCH recommends YES; treat as in-scope this phase — surfaced here as an - owner-confirmable decision, default = apply), replace the current + Per Open Q #1 (RESOLVED — CONFIRMED in-scope this phase; no owner deferral), replace the current `Environment.CommandLine.Trim().Split(' ')` tokenization inside AddCommandLine with `Environment.GetCommandLineArgs()`, which returns a properly-tokenized string[] whose [0] is the exe - path. AddCommandLine keeps passing the resulting args to the existing AddArguments (which builds the - options + CommandLineSettingsBinder), and callers relying on the default SkipFirstArgument=true then - correctly drop the exe path. Do NOT change AddArguments, the method signature, or the return. Do NOT - add quote-stripping (GetCommandLineArgs already tokenizes). This makes the "quoted value with spaces" - criterion hold end-to-end for the convenience method, not just the AddArguments path. - OWNER NOTE: if the owner defers Open Q #1, skip this task and leave AddCommandLine as-is; the D-04 - lookahead fix from Task 1 still satisfies SC #4 for callers passing already-tokenized args - (Main / GetCommandLineArgs). + path. Per D-05 (refined), AddCommandLine OWNS the exe skip: it must enable SkipFirstArgument=true for + the binder it creates (wrap the caller's action so the option is set true first, then the caller's + action runs and may override it) rather than relying on any shared default. Because AddArguments now + defaults SkipFirstArgument=false, this split ensures AddCommandLine drops the exe at [0] while direct + AddArguments(mainArgs) callers keep every token. Do NOT change AddArguments, the AddCommandLine method + signature, or the return. Do NOT add quote-stripping (GetCommandLineArgs already tokenizes). This makes + the "quoted value with spaces" criterion hold end-to-end for the convenience method UNCONDITIONALLY. + Note: Environment.GetCommandLineArgs() is not test-injectable, so AddCommandLine's exe-skip is proven + at the mechanism level by Task 1's SkipFirstArgument=true case plus this task's grep that AddCommandLine + both tokenizes via GetCommandLineArgs() and enables the skip; a direct AddCommandLine() unit test would + bind against the test runner's own argv and is intentionally not added. - cd src && grep -q 'Environment.GetCommandLineArgs()' Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs && dotnet build SimpleSettings.slnx -c Release + cd src && grep -q 'Environment.GetCommandLineArgs()' Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs && grep -Eq 'SkipFirstArgument *= *true' Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs && dotnet build SimpleSettings.slnx -c Release - SettingsBuilderFactoryExtensions.cs AddCommandLine sources its args from `Environment.GetCommandLineArgs()`. + - AddCommandLine enables `SkipFirstArgument = true` for the binder it creates (exe skip owned by this entry point, caller-overridable) — grep confirms both the tokenizer and the skip enablement. - AddArguments and the AddCommandLine signature/return are unchanged. - The solution builds with 0 errors on both TFMs and the full TUnit suite stays green. - AddCommandLine feeds properly-tokenized args; the quoted-with-spaces criterion holds end-to-end for the convenience method; suite green. + AddCommandLine feeds properly-tokenized args from GetCommandLineArgs() and drops the exe at [0] via an internally-enabled SkipFirstArgument; the quoted-with-spaces criterion holds end-to-end for the convenience method unconditionally; suite green. @@ -187,8 +226,9 @@ symbols): | Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | |-----------|----------|-----------|----------|-------------|-----------------| -| T-03-04 | Tampering | CLI arg parsing of untrusted `string[]` in `CommandLineSettingsBinder.Parse` | low | mitigate | Values are stored as strings only — no eval/command-execution sink; the lookahead adds no injection surface; downstream conversion is already secret-safe (SEC-01, unchanged). | -| T-03-05 | Information Disclosure | `AddCommandLine` reading `Environment.GetCommandLineArgs()` (exe path + args) | low | accept | Args become string config values only; SkipFirstArgument=true drops the exe path by default; no secret handling changes; consumer chooses what to bind. | +| T-03-04 | Tampering | CLI arg parsing of untrusted `string[]` in `CommandLineSettingsBinder.Parse` | low | mitigate | Values are stored as strings only — no eval/command-execution sink; the lookahead adds no injection surface; the empty-safe prefix detection removes a reachable IndexOutOfRangeException (A3); downstream conversion is already secret-safe (SEC-01, unchanged). | +| T-03-05 | Information Disclosure | `AddCommandLine` reading `Environment.GetCommandLineArgs()` (exe path + args) | low | accept | Args become string config values only; AddCommandLine enables SkipFirstArgument=true so the exe path is dropped at that entry point; no secret handling changes; consumer chooses what to bind. | +| T-03-06 | Information Disclosure | Secret-redaction invariant (SEC-01/S1) under the rewritten CLI binder | low | mitigate | Discharged explicitly: (a) the rewritten `Parse` runs at construction/config-time, OUTSIDE the `ValuesPopulator` try/catch that chains a binder throw into `SettingsBindingException` (ValuesPopulator.cs:59-61), so it cannot inject a value-bearing chained exception; (b) `BindPropertySettings` — the redaction-critical method (ISectionBinder.cs:5-7), contract "must not throw a value-bearing exception" — is NOT touched by this phase and stays store-only; (c) `Environment.GetCommandLineArgs()` adds no logging and no value-bearing throw. A CLI-path regression test (S2) binds an unconvertible sentinel and asserts it is absent from the thrown exception's full ToString() chain. | @@ -205,9 +245,11 @@ The `dotnet test --filter "*..."` form is rejected (exits 5, zero tests). Local - A quoted command-line value with spaces binds to the full value; space-separated `--k v` and inline `--k=v`/`--k:v` all bind (Phase SC #4). -- A prefixed token following a key is a new key, not a value; the valueless key is not stored (D-04). -- arg[0] is skipped by default; SkipFirstArgument=false re-includes it (Phase SC #4, D-05). -- AddCommandLine tokenizes via GetCommandLineArgs so the criterion holds end-to-end (Open Q #1). +- A prefixed token following a key is a new key, not a value; the valueless key is not stored (D-04). A space-separated value that itself starts with a prefix does not bind — intended, documented on ArgumentPrefixes (A4). +- The exe is skipped only at the AddCommandLine() entry point (which enables SkipFirstArgument=true internally); AddArguments(string[]) binds exactly what it is handed (SkipFirstArgument default false), so no real first argument is silently dropped (Phase SC #4, D-05 refined). +- AddCommandLine tokenizes via GetCommandLineArgs so the criterion holds end-to-end UNCONDITIONALLY (Open Q #1 RESOLVED). +- An empty-string token is parsed safely with no IndexOutOfRangeException (A3); prefix detection is zero-alloc per token (P1). +- Binding an unconvertible CLI value does not leak it — the thrown exception's ToString() chain excludes the value (SEC-01/S1, CLI-path regression test). - Full TUnit suite green on net8 + net10. diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-PATTERNS.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-PATTERNS.md new file mode 100644 index 0000000..5b701ba --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-PATTERNS.md @@ -0,0 +1,257 @@ +# Phase 3: Public Surface, Packaging & Binder Cleanup - Pattern Map + +**Mapped:** 2026-07-14 +**Files analyzed:** 6 modified + 2 test/verification + 3 removals +**Analogs found:** 6 / 6 (all in-repo; this phase edits existing files rather than creating new abstractions) + +> This phase is almost entirely *editing existing files in place* and *removing* surface — not +> creating new files from a template. So the "closest analog" for most edits is the file itself +> (its own surrounding conventions) plus one sibling that already does the target thing. Excerpts +> below are the concrete lines a plan should copy/modify. + +## File Classification + +| Modified/Removed/New File | Role | Data Flow | Closest Analog | Match Quality | +|---------------------------|------|-----------|----------------|---------------| +| `src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs` | model (DTO) | transform | `src/Core/ExistForAll.SimpleSettings/ISettingsHolder.cs` (already internal) | exact | +| `src/Directory.Packages.props` | config (CPM) | build | RESEARCH Pattern 1 (verified conditional `PackageVersion`) | exact (verified excerpt) | +| `src/SimpleSettings.slnx` | config (solution) | build | self (existing `` lines) | exact | +| `src/Tests/.../ExistForAll.SimpleSettings.UnitTests.csproj` | config (test proj) | build | self (existing `` lines) | exact | +| `src/Core/.../CommandLineSettingsBinder.cs` (`Parse`/`SplitByDelimiter`) | binder (service) | transform / batch | self + `SplitByDelimiter` existing logic | exact (in-place) | +| `src/Core/.../CommandLineSettingsBinderOptions.cs` (add `SkipFirstArgument`) | config (options) | request-response | self (`IsCaseSensitive` bool prop) | exact | +| `src/Core/.../SettingsBuilderFactoryExtensions.cs` (`AddCommandLine`) | extension (utility) | transform | self (`AddCommandLine` line 44) | exact — Open Q #1 | +| `src/Tests/.../Binders/CommandLine/ArgumentsTests.cs` (extend) OR new `CommandLineParseTests.cs` | test | transform | `ArgumentsTests.cs` (same dir) | exact | +| `src/Core/ExistForAll.SimpleSettings.Core.AspNet/` (REMOVE) | — | — | — | removal | + +## Pattern Assignments + +### `SettingsHolder.cs` (model — API-01, D-01) + +**Analog:** `ISettingsHolder.cs:5` (already `internal`) — mirror its accessibility. + +**The only change** (`SettingsHolder.cs:5`): +```csharp +// before +public class SettingsHolder : ISettingsHolder +// after +internal sealed class SettingsHolder : ISettingsHolder +``` + +**Convention to preserve** (rest of file, lines 1-16): block-scoped namespace, one public type +per file, ctor-assigned get-only props. No other edit — `InternalsVisibleTo` already granted in +`Info.cs`: +```csharp +[assembly:InternalsVisibleTo("ExistForAll.SimpleSettings.UnitTests")] +[assembly:InternalsVisibleTo("ExistForAll.SimpleSettings.Benchmark")] +``` +No PublicAPI analyzer baseline exists in the repo → no surface file to update. + +--- + +### `Directory.Packages.props` (config/CPM — PKG-02, D-03) + +**Analog:** RESEARCH Pattern 1 (verified live-restore excerpt). Replaces lines 8-12. + +**Current** (`Directory.Packages.props:7-13`): +```xml + + + + + + + +``` + +**Target — per-TFM conditional items** (Option 1, latest-patch-per-package; each item MUST be +mutually-exclusively conditioned or NU1504 fires): +```xml + + + + + + + + + + + +``` +net8 patches differ per package (`8.0.0 / 8.0.1 / 8.0.1 / 8.0.2`) → the conditional-item shape, +NOT the shared-property shape. Leave TUnit/BenchmarkDotNet/MinVer ItemGroups (lines 15-28) +untouched. Owner may pick uniform `8.0.0` (Option 2) instead — surface both in the plan. + +**Verification** (RESEARCH §Validation): `dotnet restore` then assert +`Core/ExistForAll.SimpleSettings/obj/project.assets.json` `targets["net8.0"]` shows +`Microsoft.Extensions.Configuration/8.0.0` (not `10.`) and `targets["net10.0"]` shows `10.0.9`. + +--- + +### `SimpleSettings.slnx` + `UnitTests.csproj` (config — PKG-01, D-02) + +**Analog:** the surrounding sibling lines (identical XML shape). + +Remove **`SimpleSettings.slnx:13`**: +```xml + +``` +Remove **`UnitTests.csproj:19`**: +```xml + +``` +Then delete the directory `src/Core/ExistForAll.SimpleSettings.Core.AspNet/` (incl. stale +`obj/`+`bin/`). The test project's own `Core/AspNet/Environments.cs` (namespace +`...UnitTests.Core.AspNet`) is a local duplicate — **leave it**; it does not reference the package. +Post-removal: unlist the published `2.0.0-alpha.0.*` alphas via +`dotnet nuget delete -s https://api.nuget.org/v3/index.json --non-interactive` +(unlists, not deletes) under the `guy-lud` account (A1 — owner-optional). + +--- + +### `CommandLineSettingsBinderOptions.cs` (options — SRC-02, D-05) + +**Analog:** the existing `IsCaseSensitive` auto-prop with default (`CommandLineSettingsBinderOptions.cs:18`): +```csharp +public bool IsCaseSensitive { get; set; } = true; +``` + +**Add, same shape** (RESEARCH §Code Examples): +```csharp +/// Skip args[0] (the executable path) when parsing. +/// Default false — AddArguments(Main(string[])) binds exactly what it is handed. +/// AddCommandLine() sets this true internally (its GetCommandLineArgs() source has the exe at [0]). +public bool SkipFirstArgument { get; set; } = false; +``` +Preserve the existing prefix/delimiter list conventions (`_argumentPrefixes`, `_delimiters`, +collection-expression init lines 9-10) — D-06 keeps all existing options unchanged. + +--- + +### `CommandLineSettingsBinder.cs` — `Parse` / `SplitByDelimiter` (binder — SRC-02, D-04) + +**Analog:** the file's own existing `Parse` loop + `SplitByDelimiter`; diverges from Microsoft's +`CommandLineConfigurationProvider` (which consumes next token *unconditionally* — SimpleSettings +does NOT). Keep the fix inside these two members; no new abstraction. + +**Existing `Parse`** (`CommandLineSettingsBinder.cs:37-54`) — a plain `foreach` with no lookahead; +this is what changes to an index/enumerator loop: +```csharp +private void Parse(string[] args) +{ + _argumentStore.Clear(); + if (args == null) return; + + foreach (var arg in args) + { + var (key, value) = SplitByDelimiter(arg, _options)!; + var name = key.TrimStart(_options.ArgumentPrefixes.ToArray()); + if (value != null) + _argumentStore[name] = value; + } +} +``` + +**Target behavior** (implement, ~15 lines): +1. If `_options.SkipFirstArgument` and `args.Length > 0`, start iteration at index 1 (D-05). +2. For each token: run `SplitByDelimiter` (existing inline `--k=v`/`--k:v` path stays). +3. If `value == null` AND the token was prefixed (had a stripped prefix) AND a next token exists + that is **not** prefixed → consume next token as value, advance index (D-04 lookahead). +4. If the next token IS prefixed (`-`/`/`) → current key stays valueless → not stored (preserves + today's "no value ⇒ skip" via the existing `if (value != null)` guard). + +**`SplitByDelimiter` stays as-is** (`:56-90`) — min-index delimiter wins, `Substring(idx+1)` is the +value (matches Microsoft's first-`=`-wins semantics). Detecting "was prefixed" reuses +`key.TrimStart(_options.ArgumentPrefixes.ToArray())` — compare length before/after trim, or check +`_options.ArgumentPrefixes.Contains(token[0])`. + +**Anti-patterns (RESEARCH):** no switch-mappings, no short-flag/boolean semantics, no manual +quote-stripping (shell already unquotes). + +--- + +### `SettingsBuilderFactoryExtensions.cs` — `AddCommandLine` (extension — SRC-02, Open Q #1) + +**Analog:** the method itself (`SettingsBuilderFactoryExtensions.cs:41-49`): +```csharp +public static T AddCommandLine(this T target, + Action? action = null) where T : ISettingsBuilderFactory +{ + var args = Environment.CommandLine.Trim().Split(' '); // <-- quote-unsafe + target.AddArguments(args, action); + return target; +} +``` + +**Recommended change (RESEARCH Open Q #1 / Pitfall 3):** replace +`Environment.CommandLine.Trim().Split(' ')` with `Environment.GetCommandLineArgs()` — properly +tokenized `string[]`, `[0]` = exe path (which `AddCommandLine` drops by setting `SkipFirstArgument=true` internally). This +makes the "quoted value with spaces" criterion true end-to-end for the convenience method. +**RESOLVED — in-scope** (Open Q #1). `AddArguments` (`:51-61`) keeps the option default `false` and binds exactly what it is handed; `AddCommandLine` wraps the caller action to set `SkipFirstArgument=true`. + +--- + +### `ArgumentsTests.cs` (test — SRC-02) + +**Analog / location:** `src/Tests/.../Binders/CommandLine/ArgumentsTests.cs` (extend in place, or +add sibling `CommandLineParseTests.cs` in the same dir). + +**Existing test shape to copy** (`ArgumentsTests.cs:10-20`): +```csharp +[Test] +public async Task Build_WhenNameMatchKey_ShouldPlaceValue() +{ + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(Args.Split(' '))); + var result = sut.GetSettings(); + await Assert.That(result.Name).IsEqualTo("value"); + await Assert.That(result.Age).IsNotEqualTo(3); +} +``` +Conventions: TUnit `[Test]`, `async Task`, `await Assert.That(...).IsEqualTo(...)`, nested +`ICommandLineInterface` with `[SettingsProperty("name")]`, fluent options via +`o => o.SetCaseSensitivity(false)`. **Fixture gotcha (Pitfall 4):** the existing `Args = +" name=value age:3"` has a leading space → `Split(' ')[0] == ""`, so under the refined default +`SkipFirstArgument=false` the empty token is parsed safely and not stored, and the suite stays +green as-is. Keep the fixture unchanged. + +**New cases to add:** `--k v` (space-separated), `--k "v with spaces"` (single already-unquoted +token), `--k --other` (prefixed next → new key, current valueless → not stored), +`SkipFirstArgument` true vs false. + +## Shared Patterns + +### File & namespace conventions +**Source:** all `src/Core/**/*.cs` +**Apply to:** every edited/new `.cs` +- One public type per file; `I`-prefixed interfaces; block-scoped `namespace ExistForAll.SimpleSettings[.Sub] { }`. +- Multi-target `net8.0;net10.0` only — validate BOTH TFMs after every change. + +### Options-object pattern +**Source:** `CommandLineSettingsBinderOptions.cs` (auto-props with defaults + fluent `Add*`/`Clear*` mutators) +**Apply to:** the new `SkipFirstArgument` option — plain `{ get; set; } = false;` mirrors the `IsCaseSensitive` bool-prop shape (the default value differs; `AddCommandLine` sets it true internally per refined D-05). + +### Section-binder contract +**Source:** `CommandLineSettingsBinder : ISectionBinder` (ctor takes args+options, `Parse` fills `_argumentStore`, `BindPropertySettings` reads it) +**Apply to:** SRC-02 stays entirely within this contract — no new abstraction. + +### CPM version policy +**Source:** `Directory.Packages.props` (`ManagePackageVersionsCentrally=true`, ``) +**Apply to:** PKG-02 — conditional items only; never mix an unconditional + conditional item for one package (NU1504). + +### Test conventions (TUnit) +**Source:** `ArgumentsTests.cs` +**Apply to:** all new binder tests. Run via `--treenode-filter "/*/*/ArgumentsTests/*"` or unfiltered +(NOT `--filter`, which exits 5 on Microsoft.Testing.Platform). + +## No Analog Found + +None — every change targets an existing file or removes one. The only "new" artifact is the PKG-02 +restore-assertion verification (RESEARCH §Validation provides a ready `python3` script; not source +code). + +## Metadata + +**Analog search scope:** `src/Core/**`, `src/Tests/**`, `src/Directory.Packages.props`, `src/SimpleSettings.slnx` +**Files scanned:** 9 (SettingsHolder, ISettingsHolder mention, Info.cs, Directory.Packages.props, slnx, UnitTests.csproj, CommandLineSettingsBinder, CommandLineSettingsBinderOptions, SettingsBuilderFactoryExtensions, ArgumentsTests) +**Pattern extraction date:** 2026-07-14 diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md index 6a81aa4..c47d676 100644 --- a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-RESEARCH.md @@ -13,7 +13,7 @@ - **D-02b:** Project is packable by SDK default. If a `Core.AspNet` prerelease alpha was pushed to NuGet, it stops publishing on removal; note deprecation/unlisting if it exists (pre-stable → low stakes). - **D-03 (PKG-02):** Float the floor **per-TFM via CPM conditional `PackageVersion`** in `src/Directory.Packages.props` (`:8-12`): **net8 → latest `8.0.x` patch**, **net10 → `10.0.x` (currently `10.0.9`)**. All four packages: `Microsoft.Extensions.Configuration`, `.Configuration.Json`, `.DependencyInjection`, `.DependencyInjection.Abstractions`. Verify build + full suite green on **both** TFMs. - **D-04 (SRC-02):** Add **space-separated `--key value`** support (lookahead pairing) *in addition to* inline `--key=value` / `--key:value`. Rule: a prefixed key followed by a **non-prefixed** token consumes it as value; a following token starting with a prefix (`-`/`/`) is a **new key**, not a value (current key then has no value → not stored, preserving today's "no value ⇒ skip" semantics). -- **D-05 (SRC-02):** **Skip `arg[0]` (exe path) by default**, exposed as a toggle on `CommandLineSettingsBinderOptions` (e.g. `SkipFirstArgument`, default `true`) so `Main(string[])` callers (which already exclude the exe) can turn it off. +- **D-05 (SRC-02) — REFINED (see "## Open Questions (RESOLVED)" below; authoritative in 03-CONTEXT.md):** Skip the exe path **at the entry point that carries it**. `AddCommandLine()` (reads `Environment.GetCommandLineArgs()`, `[0]` = exe) enables the skip internally; `AddArguments(string[])` binds exactly what it is handed. `SkipFirstArgument` on `CommandLineSettingsBinderOptions` is an explicit override with default **`false`** (a shared default `true` would silently drop `args[0]` for `AddArguments` callers). - **D-06 (SRC-02):** Preserve existing options (prefixes `-`/`/`, delimiters `:`/`=`, `IsCaseSensitive`, `NameFormatter`). No manual quote-stripping (shell already unquotes). ### Claude's Discretion @@ -44,7 +44,7 @@ All four items are small, well-scoped, and independently verifiable; three are p The one genuinely non-obvious finding: the **latest `8.0.x` patch differs per package** (`Configuration` = `8.0.0`, `.Configuration.Json` = `8.0.1`, `.DependencyInjection` = `8.0.1`, `.DependencyInjection.Abstractions` = `8.0.2`), so the four conditional entries carry three different net8 versions. Conditional `` duplicated per TFM is valid CPM and produces **no NU1504** because NuGet restore is per-TFM and evaluates the conditions — I confirmed this with a live restore that resolved net8→8.0.x and net10→10.0.9. A second finding for SRC-02: `Core.AspNet` **has been published** to NuGet.org as `1.0.0` and a long run of `2.0.0-alpha.0.N` prereleases, so removal should be paired with unlisting the alphas (low stakes, pre-stable). -For the CLI binder, the decided lookahead rule (**D-04**) *deliberately diverges* from Microsoft's `CommandLineConfigurationProvider`, which consumes the next token as a value **unconditionally** (even if it starts with a prefix). SimpleSettings instead treats a prefixed next-token as a new key. This is a correct, intentional design choice — plan the tests around SimpleSettings' rule, not exact Microsoft parity. Separately, the `AddCommandLine()` convenience method's `Environment.CommandLine.Trim().Split(' ')` is the real reason `SkipFirstArgument` defaults to `true` (arg[0] is the exe path) — and it is itself quote-unsafe (see Pitfall 3). +For the CLI binder, the decided lookahead rule (**D-04**) *deliberately diverges* from Microsoft's `CommandLineConfigurationProvider`, which consumes the next token as a value **unconditionally** (even if it starts with a prefix). SimpleSettings instead treats a prefixed next-token as a new key. This is a correct, intentional design choice — plan the tests around SimpleSettings' rule, not exact Microsoft parity. Separately, the `AddCommandLine()` convenience method's `Environment.CommandLine.Trim().Split(' ')` is the real reason `AddCommandLine()` must skip `arg[0]` (the exe path) — under refined D-05 it sets `SkipFirstArgument = true` internally (the option default is `false`) — and that split is itself quote-unsafe (see Pitfall 3), which is why it moves to `GetCommandLineArgs()`. **Primary recommendation:** Ship all four as one PR on `chore/gsd-ownership-cutover` (never direct to `master` — every master push publishes a throwaway alpha). Use conditional `PackageVersion` items (two per package) for PKG-02; validate PKG-02 by inspecting `obj/project.assets.json` per TFM. Keep the binder fix inside `Parse`/`SplitByDelimiter` + one new option; add TUnit tests under `Binders/CommandLine/`. @@ -122,7 +122,7 @@ Environment.CommandLine (exe + args) │ new CommandLineSettingsBinder(args, options) │ Parse(args): (SRC-02 change here) - ├─ [SkipFirstArgument] skip args[0] (default true) ◄── D-05 + ├─ [SkipFirstArgument] skip args[0] (AddCommandLine sets true; option default false) ◄── D-05 (refined) └─ for each arg (with lookahead): ◄── D-04 ├─ inline "--k=v" / "--k:v" → store (existing) ├─ "--k" + next non-prefixed → store k=next @@ -241,15 +241,15 @@ data[key] = value; // last-writer-wins **What goes wrong:** `AddCommandLine()` uses `Environment.CommandLine.Trim().Split(' ')` — this (a) splits a quoted value/path with spaces into multiple tokens, and (b) includes the **exe path** as `args[0]`. **Why it happens:** `Environment.CommandLine` is the raw, un-tokenized string; naive `Split(' ')` ignores quoting. **How to avoid:** -- `SkipFirstArgument` default **true** is correct for the `AddCommandLine()` path (drops the exe path). -- For the `AddArguments(Main(string[] args))` path, `Main` args **exclude** the exe, so a caller must set `SkipFirstArgument = false` or the first real arg is dropped — document this on the option. +- `AddCommandLine()` sets `SkipFirstArgument = true` internally — its `GetCommandLineArgs()` source has the exe at `[0]`. The option default is **`false`**. +- The `AddArguments(Main(string[] args))` path keeps the default `false` and binds exactly what it is handed (`Main` args already exclude the exe) — no silent `arg[0]` drop. Document the option's behavior either way. - **Strongly consider** switching `AddCommandLine()` to `Environment.GetCommandLineArgs()` (properly tokenized, `[0]` = exe) so D-04's quoted-value fix actually helps the `AddCommandLine()` path too. This is arguably in-scope for "quoted value with spaces binds correctly" (see Open Questions). **Warning signs:** `--name "John Doe"` binds `Name="John"` (or nothing) instead of `John Doe`. ### Pitfall 4: existing `ArgumentsTests` under the new default (SRC-02) -**What goes wrong:** Adding `SkipFirstArgument` default-true silently drops a real first arg and breaks a test. +**What goes wrong (historical — resolved by refined D-05):** A *shared* `SkipFirstArgument` default-true silently drops a real first arg for `AddArguments`. Refined D-05 defaults the option to `false` (only `AddCommandLine` sets it true internally), so this no longer occurs. **Why it happens (and why it's fine here):** The existing test uses `" name=value age:3".Split(' ')` = `["", "name=value", "age:3"]` — the **leading space** makes `args[0]` an empty string. Skipping `""` is harmless; `name`/`age` are at `[1]`/`[2]`. The suite stays green. -**How to avoid:** Keep the leading-space fixture as-is, **or** rewrite it to `["name=value","age:3"]` and pass `SkipFirstArgument=false`. Either preserves intent. Add new cases for the space-separated form. +**How to avoid:** With the refined default `false`, the existing leading-space fixture stays green as-is (arg[0] is `""`, parsed safely and not stored). Add new cases for the space-separated form. **Warning signs:** `Build_WhenNameMatchKey_ShouldPlaceValue` fails after the option lands. ### Pitfall 5: TUnit filter (project-wide, from TESTING.md) @@ -282,7 +282,7 @@ The test project's own `Core/AspNet/Environments.cs` (namespace `...UnitTests.Co /// Skip args[0] (the executable path) when parsing. /// Default true — correct for Environment.CommandLine / GetCommandLineArgs(). /// Set false when passing Main(string[] args), which already excludes the exe. -public bool SkipFirstArgument { get; set; } = true; +public bool SkipFirstArgument { get; set; } = false; // AddCommandLine() sets true internally (refined D-05) ``` ## State of the Art diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VALIDATION.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VALIDATION.md index 516791c..867d652 100644 --- a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VALIDATION.md +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VALIDATION.md @@ -1,15 +1,18 @@ --- phase: 3 slug: public-surface-packaging-binder-cleanup -status: draft -nyquist_compliant: false -wave_0_complete: false +status: approved +nyquist_compliant: true +wave_0_complete: true created: 2026-07-14 --- # Phase 3 — Validation Strategy > Per-phase validation contract for feedback sampling during execution. +> Authored from `03-RESEARCH.md` "## Validation Architecture", with the PKG-02 floor check +> corrected per architect finding A1 (inspect the projects that actually reference the packages, +> not the core lib, and fail on zero `microsoft.extensions.*` entries). --- @@ -17,20 +20,24 @@ created: 2026-07-14 | Property | Value | |----------|-------| -| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} | -| **Config file** | {path or "none — Wave 0 installs"} | -| **Quick run command** | `{quick command}` | -| **Full suite command** | `{full command}` | -| **Estimated runtime** | ~{N} seconds | +| **Framework** | TUnit 1.58.0 on Microsoft.Testing.Platform | +| **Config file** | `src/global.json` (`"test": { "runner": "Microsoft.Testing.Platform" }`) | +| **Quick run command** | `cd src && dotnet test SimpleSettings.slnx -c Release --treenode-filter "/*/*/ArgumentsTests/*"` | +| **Full suite command** | `cd src && dotnet restore SimpleSettings.slnx && dotnet build SimpleSettings.slnx -c Release --no-restore -p:ContinuousIntegrationBuild=true && dotnet test SimpleSettings.slnx -c Release --no-build` | +| **Estimated runtime** | ~30–60 s (net10 local; net8 build-only locally, both TFMs in CI) | + +> **TUnit filter caveat:** targeted runs use `--treenode-filter "/*/*/ClassNameTests/*"`. The +> `dotnet test --filter "*Name*"` form is REJECTED by Microsoft.Testing.Platform (exits 5, zero +> tests — looks green at a glance). Run `dotnet` from `src/` (global.json pins the platform). --- ## Sampling Rate -- **After every task commit:** Run `{quick run command}` -- **After every plan wave:** Run `{full suite command}` -- **Before `/gsd-verify-work`:** Full suite must be green -- **Max feedback latency:** {N} seconds +- **After every task commit:** Run the touched task's `` verify (targeted `--treenode-filter` for test tasks; `grep` + scoped `dotnet build` for source/packaging tasks). +- **After every plan wave:** Run the full suite command on both TFMs (CI runs net8 + net10). +- **Before `/gsd-verify-work`:** Full suite green on both TFMs AND the PKG-02 restore/floor + NuGet-audit assertion green. +- **Max feedback latency:** ~60 seconds. --- @@ -38,19 +45,21 @@ created: 2026-07-14 | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | +| 03-01-01 | 01 | 1 | API-01 | — | Public-surface reduction (net-positive) | source + build | `grep -q 'internal sealed class SettingsHolder' Core/ExistForAll.SimpleSettings/SettingsHolder.cs && dotnet build Core/ExistForAll.SimpleSettings/…csproj -c Release` | ✅ | ⬜ pending | +| 03-01-02 | 01 | 1 | PKG-01 | — | Removes a dormant, unserviced package id (unlist is owner-manual) | build | `test ! -e …Core.AspNet && test -f Tests/…/Core/AspNet/Environments.cs && ! grep -rq "Core.AspNet" SimpleSettings.slnx …UnitTests.csproj && dotnet restore && dotnet build SimpleSettings.slnx -c Release` | ✅ | ⬜ pending | +| 03-01-03 | 01 | 1 | PKG-02 | T-03-02 | No vulnerable floor: `NU1901`–`NU1904` restore-audit gate | restore + build + assets inspection | `dotnet restore -p:NuGetAudit=true -p:NuGetAuditMode=all` (fail on any `NU190[1-4]`) + `dotnet build … -p:TreatWarningsAsErrors=true` + floor check over **Binders** (`Configuration`) & **GenericHost** (`DependencyInjection.Abstractions`) `project.assets.json`, guard fails on zero `microsoft.extensions.*` per TFM | ✅ | ⬜ pending | +| 03-02-01 | 02 | 2 | SRC-02 | T-03-06 | An unconvertible CLI value never leaks into the exception `ToString()` chain (SEC-01 / S1) | unit (TUnit) | `dotnet test SimpleSettings.slnx -c Release --treenode-filter "/*/*/ArgumentsTests/*"` | ✅ (extended in place) | ⬜ pending | +| 03-02-02 | 02 | 2 | SRC-02 | — | `AddCommandLine()` tokenizes safely; exe-skip owned only by that entry point | source + build | `grep -q 'Environment.GetCommandLineArgs()' …SettingsBuilderFactoryExtensions.cs && grep -Eq 'SkipFirstArgument *= *true' …SettingsBuilderFactoryExtensions.cs && dotnet build SimpleSettings.slnx -c Release` | ✅ | ⬜ pending | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* +**SC → task coverage:** SC #1 → 03-01-01 (+ full-suite green); SC #2 → 03-01-02; SC #3 → 03-01-03; SC #4 → 03-02-01 (parse behavior + quoted-value + redaction) & 03-02-02 (`AddCommandLine` end-to-end). + --- ## Wave 0 Requirements -- [ ] `{tests/test_file.py}` — stubs for REQ-{XX} -- [ ] `{tests/conftest.py}` — shared fixtures -- [ ] `{framework install}` — if no framework detected - -*If none: "Existing infrastructure covers all phase requirements."* +*Existing infrastructure covers all phase requirements.* TUnit + Microsoft.Testing.Platform are already present; `Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs` exists and is extended in place by task 03-02-01 (class name kept stable for the treenode-filter); the PKG-02 restore/floor + audit assertion is added directly inside task 03-01-03. No framework install and no separate test-stub wave are needed. --- @@ -58,19 +67,17 @@ created: 2026-07-14 | Behavior | Requirement | Why Manual | Test Instructions | |----------|-------------|------------|-------------------| -| {behavior} | REQ-{XX} | {reason} | {steps} | - -*If none: "All phase behaviors have automated verification."* +| Published `ExistForAll.SimpleSettings.Core.AspNet` `2.0.0-alpha.0.*` prereleases are unlisted | PKG-01 (D-02b) | Registry-side action requiring the guy-lud publishing account; not automatable in-repo and owner-optional (not required for phase completion) | `dotnet nuget delete ExistForAll.SimpleSettings.Core.AspNet -s https://api.nuget.org/v3/index.json --non-interactive` under the guy-lud account (unlists — does not delete) | --- ## Validation Sign-Off -- [ ] All tasks have `` verify or Wave 0 dependencies -- [ ] Sampling continuity: no 3 consecutive tasks without automated verify -- [ ] Wave 0 covers all MISSING references -- [ ] No watch-mode flags -- [ ] Feedback latency < {N}s -- [ ] `nyquist_compliant: true` set in frontmatter +- [x] All tasks have `` verify or Wave 0 dependencies — every task (03-01-01…03-02-02) carries a real `` verify. +- [x] Sampling continuity: no 3 consecutive tasks without automated verify. +- [x] Wave 0 covers all MISSING references — N/A (no MISSING; infrastructure present). +- [x] No watch-mode flags. +- [x] Feedback latency < 60s. +- [x] `nyquist_compliant: true` set in frontmatter. -**Approval:** {pending / approved YYYY-MM-DD} +**Approval:** approved 2026-07-14 From 1d2349e10fa39d1cfaa1a88a73d7dbaa27966578 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:40:43 +0300 Subject: [PATCH 09/21] refactor(03-01): make SettingsHolder internal sealed (API-01/D-01) - Flip public class SettingsHolder to internal sealed - Removes an internal DTO from the public API surface - Reachable from tests/benchmark via existing InternalsVisibleTo --- src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs b/src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs index d60dc8f..704c02a 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs @@ -2,7 +2,7 @@ namespace ExistForAll.SimpleSettings { - public class SettingsHolder : ISettingsHolder + internal sealed class SettingsHolder : ISettingsHolder { public SettingsHolder(Type settingsType, object settingsImplementation) { From 774bc0ef529b5e8349743f03a38f857eb3cc906e Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:41:50 +0300 Subject: [PATCH 10/21] chore(03-01): remove dead Core.AspNet package (PKG-01/D-02) - Drop Core.AspNet Project entry from SimpleSettings.slnx - Drop Core.AspNet ProjectReference from UnitTests.csproj - Delete src/Core/ExistForAll.SimpleSettings.Core.AspNet/ directory - Test-local Core/AspNet/Environments.cs duplicate untouched - Published 2.0.0-alpha.0.* alphas: unlist is owner-optional follow-up --- .../Environments.cs | 9 --------- .../ExistForAll.SimpleSettings.Core.AspNet.csproj | 8 -------- src/SimpleSettings.slnx | 1 - .../ExistForAll.SimpleSettings.UnitTests.csproj | 1 - 4 files changed, 19 deletions(-) delete mode 100644 src/Core/ExistForAll.SimpleSettings.Core.AspNet/Environments.cs delete mode 100644 src/Core/ExistForAll.SimpleSettings.Core.AspNet/ExistForAll.SimpleSettings.Core.AspNet.csproj diff --git a/src/Core/ExistForAll.SimpleSettings.Core.AspNet/Environments.cs b/src/Core/ExistForAll.SimpleSettings.Core.AspNet/Environments.cs deleted file mode 100644 index 050800a..0000000 --- a/src/Core/ExistForAll.SimpleSettings.Core.AspNet/Environments.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace ExistForAll.SimpleSettings.Core.AspNet -{ - internal static class Environments - { - public const string Development = "Development"; - public const string Staging = "Staging"; - public const string Production = "Production"; - } -} \ No newline at end of file diff --git a/src/Core/ExistForAll.SimpleSettings.Core.AspNet/ExistForAll.SimpleSettings.Core.AspNet.csproj b/src/Core/ExistForAll.SimpleSettings.Core.AspNet/ExistForAll.SimpleSettings.Core.AspNet.csproj deleted file mode 100644 index da32c62..0000000 --- a/src/Core/ExistForAll.SimpleSettings.Core.AspNet/ExistForAll.SimpleSettings.Core.AspNet.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - net8.0;net10.0 - - - - - diff --git a/src/SimpleSettings.slnx b/src/SimpleSettings.slnx index 52e4de5..e96c1d8 100644 --- a/src/SimpleSettings.slnx +++ b/src/SimpleSettings.slnx @@ -10,7 +10,6 @@ - diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj b/src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj index 966f08d..0804179 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj @@ -16,7 +16,6 @@ - From 2db3bdf9845a6d370542412e06500240fd659e45 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:42:57 +0300 Subject: [PATCH 11/21] chore(03-01): float Microsoft.Extensions.* floor per-TFM (PKG-02/D-03) - Split four Microsoft.Extensions.* pins into per-TFM conditional ItemGroups - net8 floors: Configuration 8.0.0, Configuration.Json 8.0.1, DI 8.0.1, DI.Abstractions 8.0.2 - net10 stays 10.0.9 for all four - Frees net8 consumers of the packable Binders + GenericHost packages from 10.x --- src/Directory.Packages.props | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d226146..0566505 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,8 +4,16 @@ true - - + + + + + + + + + + From d27602d9e7f2b9f16b17af07cc04427d2379a952 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:44:58 +0300 Subject: [PATCH 12/21] docs(03-01): complete public-surface/packaging/binder-cleanup plan --- .planning/REQUIREMENTS.md | 12 +- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 23 +-- .../03-01-SUMMARY.md | 162 ++++++++++++++++++ 4 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-01-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index d592309..0a583c0 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -52,9 +52,9 @@ Remaining open work (from `FIX-PLAN.md`), batched toward the first `v2.0.0-beta` ### Public Surface, Packaging & Binder Cleanup -- [ ] **API-01**: Make `SettingsHolder`/`ISettingsHolder` internal (A5, breaking) -- [ ] **PKG-01**: `Core.AspNet` exposes a public type or the package is dropped (A3) -- [ ] **PKG-02**: Float `Microsoft.Extensions.*` floor per-TFM (`8.0.x` for net8) or justify the pin (A4) +- [x] **API-01**: Make `SettingsHolder`/`ISettingsHolder` internal (A5, breaking) +- [x] **PKG-01**: `Core.AspNet` exposes a public type or the package is dropped (A3) +- [x] **PKG-02**: Float `Microsoft.Extensions.*` floor per-TFM (`8.0.x` for net8) or justify the pin (A4) - [ ] **SRC-02**: Command-line binder parses quoted values with spaces correctly and skips `arg[0]` (A6) ### AOT/Trim & Documentation @@ -100,9 +100,9 @@ Deferred / held. Tracked but not in the current roadmap. | TEST-02 | Phase 2 | Complete | | TEST-03 | Phase 2 | Complete | | ENG-01 | Phase 2 | ✓ Complete (#29) | -| API-01 | Phase 3 | Pending | -| PKG-01 | Phase 3 | Pending | -| PKG-02 | Phase 3 | Pending | +| API-01 | Phase 3 | Complete | +| PKG-01 | Phase 3 | Complete | +| PKG-02 | Phase 3 | Complete | | SRC-02 | Phase 3 | Pending | | AOT-01 | Phase 4 | Pending | | DOC-01 | Phase 4 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index aa6ae67..e7bc779 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -76,10 +76,10 @@ value: config → typed settings maps accurately, and never leaks a secret doing 3. A net8 consumer is no longer transitively forced onto `Microsoft.Extensions.* 10.x` (per-TFM floor), or the pin is documented with justification. 4. A quoted command-line value containing spaces binds correctly and the executable path (`arg[0]`) is skipped. -**Plans**: 2 plans +**Plans**: 1/2 plans executed **Wave 1** -- [ ] 03-01-PLAN.md — Public surface & packaging cleanup: SettingsHolder → internal sealed (API-01), drop dead Core.AspNet package (PKG-01), per-TFM Microsoft.Extensions.* floor (PKG-02) [Wave 1] +- [x] 03-01-PLAN.md — Public surface & packaging cleanup: SettingsHolder → internal sealed (API-01), drop dead Core.AspNet package (PKG-01), per-TFM Microsoft.Extensions.* floor (PKG-02) [Wave 1] **Wave 2** *(blocked on Wave 1 completion)* @@ -121,6 +121,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 |-------|----------------|--------|-----------| | 1. Exception Safety & Public Hierarchy | n/a (shipped) | ✓ Complete | 2026-07-14 (#27/#28) | | 2. Binding Correctness & Engine Test Hardening | 2/2 | Complete | 2026-07-14 | -| 3. Public Surface, Packaging & Binder Cleanup | 0/2 | Not started | - | +| 3. Public Surface, Packaging & Binder Cleanup | 1/2 | In Progress| | | 4. AOT/Trim Honesty & Documentation | 0/TBD | Not started | - | | 5. First v2.0.0-beta Release | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 3295669..a79b9fc 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,18 +2,18 @@ gsd_state_version: 1.0 milestone: v2.0.0 milestone_name: milestone -current_phase: 3 -current_phase_name: Public Surface, Packaging & Binder Cleanup +current_phase: 03 +current_phase_name: public-surface-packaging-binder-cleanup status: executing stopped_at: Phase 3 context gathered -last_updated: "2026-07-14T12:32:26.326Z" +last_updated: "2026-07-14T12:44:42.924Z" last_activity: 2026-07-14 -last_activity_desc: Phase 02 complete, transitioned to Phase 3 +last_activity_desc: Phase 03 execution started progress: total_phases: 5 completed_phases: 1 - total_plans: 2 - completed_plans: 2 + total_plans: 4 + completed_plans: 3 percent: 20 --- @@ -24,14 +24,14 @@ progress: See: .planning/PROJECT.md (updated 2026-07-13) **Core value:** Correctness of binding — config → strongly-typed settings maps accurately across every supported shape (sections, arrays/enumerables, defaults, nullable, custom converters). -**Current focus:** Phase 02 — binding-correctness-engine-test-hardening +**Current focus:** Phase 03 — public-surface-packaging-binder-cleanup ## Current Position -Phase: 3 — Public Surface, Packaging & Binder Cleanup -Plan: Not started +Phase: 03 (public-surface-packaging-binder-cleanup) — EXECUTING +Plan: 2 of 2 Status: Ready to execute -Last activity: 2026-07-14 — Phase 02 complete, transitioned to Phase 3 +Last activity: 2026-07-14 — Phase 03 execution started Progress: [██░░░░░░░░] 20% @@ -57,6 +57,7 @@ Progress: [██░░░░░░░░] 20% *Updated after each plan completion* | Phase 02 P01 | 2min | 3 tasks | 2 files | | Phase 02 P02 | 3min | 1 tasks | 1 files | +| Phase 03 P01 | 3min | 3 tasks | 4 files | ## Accumulated Context @@ -95,6 +96,6 @@ Items acknowledged and carried forward: ## Session Continuity -Last session: 2026-07-14T10:59:02.037Z +Last session: 2026-07-14T12:44:37.068Z Stopped at: Phase 3 context gathered Resume file: .planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-SUMMARY.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-SUMMARY.md new file mode 100644 index 0000000..20ee343 --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-01-SUMMARY.md @@ -0,0 +1,162 @@ +--- +phase: 03-public-surface-packaging-binder-cleanup +plan: 01 +subsystem: packaging +tags: [dotnet, nuget, cpm, central-package-management, public-api, multi-targeting, net8, net10] + +# Dependency graph +requires: + - phase: 02 + provides: modern-only TFM baseline (net8.0;net10.0), CPM via Directory.Packages.props, MinVer versioning, TUnit test stack +provides: + - SettingsHolder is internal sealed (removed from the public API surface) + - Core.AspNet dead package removed from the solution graph and all references + - per-TFM conditional Microsoft.Extensions.* floors (net8 8.0.x / net10 10.0.9) +affects: [03-02, packaging, public-surface, beta-release] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Per-TFM conditional PackageVersion ItemGroups in CPM (mutually-exclusive $(TargetFramework) gates)" + - "Floor verification against the packable consumer projects' project.assets.json with a zero-entry guard" + +key-files: + created: [] + modified: + - src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs + - src/SimpleSettings.slnx + - src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj + - src/Directory.Packages.props + +key-decisions: + - "SettingsHolder flipped to internal sealed (API-01/D-01) — reachable from tests/benchmark via existing InternalsVisibleTo; no public-API baseline file to update" + - "Core.AspNet removed from repo (PKG-01/D-02); published 2.0.0-alpha.0.* alphas left as owner-optional unlist follow-up (D-02b), not automated" + - "net8 floors set at latest-patch-per-package Option 1 (Configuration 8.0.0, Configuration.Json 8.0.1, DI 8.0.1, DI.Abstractions 8.0.2); net10 stays 10.0.9 (PKG-02/D-03)" + - "Per-TFM condition placed on the ItemGroup rather than each item — functionally mutually-exclusive, avoids NU1504, cleaner than per-item duplication" + +patterns-established: + - "Per-TFM conditional PackageVersion ItemGroups: two mutually-exclusive groups gated on $(TargetFramework), one item per package per group, no unconditional twin (NU1504-safe)" + - "Consumer-floor assertion targets the packable projects (Binders/GenericHost) assets, not the core lib which references no Microsoft.Extensions.* — with a GUARD-zero-ms-ext check to prevent a vacuous green" + +requirements-completed: [API-01, PKG-01, PKG-02] + +coverage: + - id: D1 + description: "SettingsHolder is internal sealed — removed from the public API surface, reachable from tests only via InternalsVisibleTo" + requirement: "API-01" + verification: + - kind: unit + ref: "dotnet test SimpleSettings.slnx -c Release --framework net10.0 (94/94 passed — tests reach SettingsHolder via InternalsVisibleTo)" + status: pass + - kind: other + ref: "grep 'internal sealed class SettingsHolder' src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs + Release build 0 errors both TFMs" + status: pass + human_judgment: false + - id: D2 + description: "Core.AspNet is absent from the solution and no project references it; the solution builds without it" + requirement: "PKG-01" + verification: + - kind: other + ref: "test ! -e src/Core/ExistForAll.SimpleSettings.Core.AspNet && grep -rq Core.AspNet returns none in slnx + UnitTests.csproj" + status: pass + - kind: integration + ref: "dotnet restore + dotnet build SimpleSettings.slnx -c Release (both TFMs, 0 errors)" + status: pass + human_judgment: false + - id: D3 + description: "Microsoft.Extensions.Configuration (Binders package) and DependencyInjection.Abstractions (GenericHost package) resolve to 8.0.x on net8 and 10.0.9 on net10" + requirement: "PKG-02" + verification: + - kind: integration + ref: "project.assets.json floor check on Binders + GenericHost — floor-check-errors: [] (net8 8.0.0/8.0.2, net10 10.0.9)" + status: pass + - kind: integration + ref: "dotnet restore -p:NuGetAuditMode=all — no NU1901-NU1904; build -p:TreatWarningsAsErrors=true 0 warnings both TFMs" + status: pass + human_judgment: false + - id: D4 + description: "Unlist the published Core.AspNet 2.0.0-alpha.0.* prereleases on NuGet.org (owner-optional registry hygiene)" + requirement: "PKG-01" + verification: [] + human_judgment: true + rationale: "Requires the guy-lud publishing account and a manual dotnet nuget delete against NuGet.org; deliberately NOT automated by this plan (D-02b). Owner-optional, not required for phase completion." + +# Metrics +duration: 3min +completed: 2026-07-14 +status: complete +--- + +# Phase 3 Plan 01: Public Surface + Packaging + Binder Cleanup Summary + +**SettingsHolder made internal sealed, dead Core.AspNet package removed from the solution graph, and Microsoft.Extensions.* floors floated per-TFM so net8 consumers resolve 8.0.x while net10 stays 10.0.9 — full TUnit suite green.** + +## Performance + +- **Duration:** ~3 min +- **Started:** 2026-07-14T12:40:27Z +- **Completed:** 2026-07-14T12:43:25Z +- **Tasks:** 3 +- **Files modified:** 4 (+ 1 package directory removed) + +## Accomplishments +- `SettingsHolder` flipped from `public class` to `internal sealed class` — an internal DTO removed from the public API surface (API-01/D-01), still reachable from tests + benchmark via the existing `InternalsVisibleTo`. +- `Core.AspNet` dead package fully removed: dropped from `SimpleSettings.slnx`, dropped from the UnitTests `ProjectReference` set, and its `src/Core/ExistForAll.SimpleSettings.Core.AspNet/` directory deleted (PKG-01/D-02). The test project's local `Core/AspNet/Environments.cs` duplicate (different namespace) was preserved. +- Four `Microsoft.Extensions.*` pins split into two mutually-exclusive per-TFM conditional `ItemGroup`s: net8 floors (Configuration 8.0.0, Configuration.Json 8.0.1, DI 8.0.1, DI.Abstractions 8.0.2), net10 all at 10.0.9 (PKG-02/D-03) — freeing net8 consumers of the packable Binders + GenericHost packages from being forced onto 10.x. +- Plan-completion gate green: CI build clean on both TFMs, restore audit (`NuGetAuditMode=all`) with no NU1901-NU1904 advisories, warnings-as-errors build 0 warnings, and the full TUnit suite passing 94/94 on net10. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Flip SettingsHolder to internal sealed (API-01/D-01)** - `1d2349e` (refactor) +2. **Task 2: Remove the dead Core.AspNet package (PKG-01/D-02)** - `774bc0e` (chore) +3. **Task 3: Float Microsoft.Extensions.* floor per-TFM (PKG-02/D-03)** - `2db3bdf` (chore) + +_Task 2's commit was amended once to fold in the slnx + csproj edits after the initial commit captured only the git-tracked deletions (see Issues Encountered)._ + +## Files Created/Modified +- `src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs` - `public class` -> `internal sealed class SettingsHolder : ISettingsHolder` +- `src/SimpleSettings.slnx` - removed the Core.AspNet `` entry +- `src/Tests/ExistForAll.SimpleSettings.UnitTests/ExistForAll.SimpleSettings.UnitTests.csproj` - removed the Core.AspNet `` +- `src/Directory.Packages.props` - replaced the four unconditional Microsoft.Extensions.* pins with two per-TFM conditional ItemGroups +- `src/Core/ExistForAll.SimpleSettings.Core.AspNet/` - directory removed (Environments.cs + .csproj deleted via git rm; leftover bin/obj removed) + +## Decisions Made +- **Per-TFM condition on the ItemGroup, not per-item.** The plan described per-item `Condition` attributes; using two mutually-exclusive `` blocks is functionally identical (only one item per package is active per TFM, NU1504-safe) and cleaner. Verified equivalent: floor check resolves net8 8.0.x / net10 10.0.9 with `floor-check-errors: []`. +- **net8 floor = Option 1 (latest-patch-per-package).** Shipped per the decision wording; the uniform-8.0.0 alternative was left unused. +- **NuGet unlist not executed.** The published `Core.AspNet` 2.0.0-alpha.0.* alphas remain listed; unlisting is captured as an owner-optional follow-up (D-02b) requiring the guy-lud publishing account. + +## Deviations from Plan + +None - plan executed exactly as written. (One process hiccup during committing, not a plan deviation — see Issues Encountered.) + +## Issues Encountered +- **Task 2 commit split.** The combined `git add` for Task 2 included the already-deleted directory pathspec, which returned a fatal and broke the `&&` chain, so the first commit (`c59d7b7`) captured only the git-tracked deletions. The commit was immediately amended (`774bc0e`) to fold in the `SimpleSettings.slnx` and UnitTests `.csproj` edits. Final commit contains all four file changes; verified via `git show --stat`. +- **Directory removal under sandbox.** `rm -rf` on the Core.AspNet directory tripped a bash pre-guard; removed the gitignored `bin/`+`obj/` and the empty directory via scoped `rm -rf bin obj` + `rmdir` instead. Directory confirmed absent. + +## User Setup Required + +**Owner-optional NuGet registry hygiene (NOT required for phase completion).** After this removal the published `ExistForAll.SimpleSettings.Core.AspNet` 2.0.0-alpha.0.* prereleases stop updating but remain listed on NuGet.org. To unlist them (unlists, does not delete): + +``` +dotnet nuget delete ExistForAll.SimpleSettings.Core.AspNet -s https://api.nuget.org/v3/index.json --non-interactive +``` + +Requires the guy-lud publishing account. This plan deliberately did NOT execute any publish/unlist (D-02b). + +## Next Phase Readiness +- Phase 3 success criteria #1 (public surface trimmed), #2 (Core.AspNet removed), and #3 (per-TFM floors) are satisfied and suite-verified on both TFMs. +- Ready for Plan 03-02. +- No blockers. The only outstanding item is the owner-optional NuGet unlist above. + +## Self-Check: PASSED + +- FOUND: 03-01-SUMMARY.md, SettingsHolder.cs, Directory.Packages.props +- REMOVED-OK: Core.AspNet directory absent +- FOUND commits: 1d2349e, 774bc0e, 2db3bdf + +--- +*Phase: 03-public-surface-packaging-binder-cleanup* +*Completed: 2026-07-14* From a9558fb6374333c75d05e1053854fcabafe8ceb6 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:52:27 +0300 Subject: [PATCH 13/21] test(03-02): add failing CLI binder cases for lookahead + SkipFirstArgument - space-separated --k v, quoted-with-spaces single token - prefixed-next-token = new key, prefixed-value non-binding - SkipFirstArgument true/false, empty-token no-crash - CLI-path secret-redaction regression (S2) --- .../Binders/CommandLine/ArgumentsTests.cs | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs index 578f957..3cfb6bc 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs @@ -6,6 +6,7 @@ namespace ExistForAll.SimpleSettings.UnitTests.Binders.CommandLine public class ArgumentsTests { private const string Args = " name=value age:3"; + private const string CliSecretSentinel = "cli-S3CR3T-do-not-log"; [Test] public async Task Build_WhenNameMatchKey_ShouldPlaceValue() @@ -59,6 +60,129 @@ public async Task Build_WhenArgumentAreAfterInMemory_ShouldUseArgumentBinder() await Assert.That(result.Age).IsEqualTo(3); } + [Test] + public async Task Build_WhenInlineEqualsAtIndexZero_BindsWithoutDrop() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--name=value" })); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsEqualTo("value"); + } + + [Test] + public async Task Build_WhenInlineColon_BindsValue() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--name:value" })); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsEqualTo("value"); + } + + [Test] + public async Task Build_WhenSpaceSeparated_BindsValue() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--name", "value" })); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsEqualTo("value"); + } + + [Test] + public async Task Build_WhenSpaceSeparatedValueHasSpaces_BindsFullValue() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--name", "John Doe" })); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsEqualTo("John Doe"); + } + + [Test] + public async Task Build_WhenNextTokenIsPrefixed_KeyIsNotStored() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--name", "--other" })); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsNull(); + } + + [Test] + public async Task Build_WhenSkipFirstArgumentFalse_BindsIndexZero() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--name", "value" })); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsEqualTo("value"); + } + + [Test] + public async Task Build_WhenSkipFirstArgumentTrue_SkipsIndexZero() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--name", "value" }, + o => o.SkipFirstArgument = true)); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsNull(); + } + + [Test] + public async Task Build_WhenEmptyToken_ParsesSafelyAndBindsRest() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "", "--name", "value" })); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsEqualTo("value"); + } + + [Test] + public async Task Build_WhenSpaceSeparatedValueIsPrefixed_DoesNotBind() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--name", "/etc/hosts" })); + + var result = sut.GetSettings(); + + await Assert.That(result.Name).IsNull(); + } + + [Test] + public async Task Build_WhenCliValueUnconvertible_ExceptionExcludesValue() + { + var sut = SettingsBuilder.CreateBuilder(x => + x.AddArguments(new[] { "--Age=" + CliSecretSentinel })); + + SettingsPropertyValueException? captured = null; + try + { + sut.GetSettings(); + } + catch (SettingsPropertyValueException e) + { + captured = e; + } + + await Assert.That(captured).IsNotNull(); + var full = captured!.ToString(); + await Assert.That(full.Contains(CliSecretSentinel)).IsFalse(); + await Assert.That(full.Contains(nameof(ICommandLineInterface.Age))).IsTrue(); + await Assert.That(full.Contains(nameof(System.Int32))).IsTrue(); + } + public interface ICommandLineInterface { [SettingsProperty("name")] From 29f546946eb6ad8cfc89cb023f6d00537c620c58 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:53:03 +0300 Subject: [PATCH 14/21] feat(03-02): add SkipFirstArgument option + lookahead CLI parse (SRC-02/D-04,D-05) - SkipFirstArgument (default false, explicit override; no silent arg[0] drop) - Parse rewritten to index/lookahead loop; space-separated --k v binds - empty-safe zero-alloc prefix detection (length-delta + Array.IndexOf, cached prefix array) - prefixed next-token treated as new key; ArgumentPrefixes XML-doc note --- .../CommandLineSettingsBinder.cs | 21 ++++++++++++++++--- .../CommandLineSettingsBinderOptions.cs | 8 +++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs index 70fd3c3..8ca34f0 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs @@ -40,16 +40,31 @@ private void Parse(string[] args) if (args == null) return; - foreach (var arg in args) + var prefixes = _options.ArgumentPrefixes.ToArray(); + var start = _options.SkipFirstArgument && args.Length > 0 ? 1 : 0; + + for (var index = start; index < args.Length; index++) { - var (key, value) = SplitByDelimiter(arg, _options)!; + var split = SplitByDelimiter(args[index], _options); + if (split == null) continue; - var name = key.TrimStart(_options.ArgumentPrefixes.ToArray()); + var (key, value) = split; + var name = key.TrimStart(prefixes); if (value != null) { _argumentStore[name] = value; + continue; } + + var tokenWasPrefixed = name.Length != key.Length; + if (!tokenWasPrefixed || index + 1 >= args.Length) continue; + + var next = args[index + 1]; + if (next.Length > 0 && Array.IndexOf(prefixes, next[0]) >= 0) continue; + + _argumentStore[name] = next; + index++; } } diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs index 47c8673..5a51ded 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs @@ -11,12 +11,20 @@ public class CommandLineSettingsBinderOptions public NameFormatter? NameFormatter { get; set; } + /// Characters that mark a token as a key (default '-' and '/'). A space-separated value + /// whose first character is one of these is treated as a new key rather than the previous key's + /// value, so it does not bind; use the inline delimiter form to bind such a value. public IEnumerable ArgumentPrefixes => _argumentPrefixes; public IEnumerable Delimiters => _delimiters; public bool IsCaseSensitive { get; set; } = true; + /// Skip args[0] (the executable path) when parsing. Default false so AddArguments binds + /// exactly what it is handed (Main(string[]) already excludes the exe); AddCommandLine sets this + /// true internally because Environment.GetCommandLineArgs() carries the exe at index 0. + public bool SkipFirstArgument { get; set; } = false; + public void AddArgumentPrefix(char prefix) { if (prefix <= 0) From ac537cfa33836f793951c1dfe8c3a5fccb80b2c8 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:53:25 +0300 Subject: [PATCH 15/21] feat(03-02): AddCommandLine sources GetCommandLineArgs + owns exe skip (SRC-02/Open Q #1) - replace Environment.CommandLine.Split(' ') with Environment.GetCommandLineArgs() - wrap caller action to enable SkipFirstArgument=true (exe skip owned by this entry point) - quoted-value-with-spaces criterion now holds end-to-end unconditionally --- .../SettingsBuilderFactoryExtensions.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs index 594036b..073c136 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs @@ -41,9 +41,13 @@ public static T AddEnvironmentVariable(this T target, public static T AddCommandLine(this T target, Action? action = null) where T : ISettingsBuilderFactory { - var args = Environment.CommandLine.Trim().Split(' '); + var args = Environment.GetCommandLineArgs(); - target.AddArguments(args, action); + target.AddArguments(args, options => + { + options.SkipFirstArgument = true; + action?.Invoke(options); + }); return target; } From d5f9b1513656a6de197ad8e90202ac9aa5963d81 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 15:56:12 +0300 Subject: [PATCH 16/21] docs(03-02): complete command-line binder cleanup plan --- .planning/REQUIREMENTS.md | 4 +- .planning/ROADMAP.md | 8 +- .planning/STATE.md | 18 +- .../03-02-SUMMARY.md | 162 ++++++++++++++++++ 4 files changed, 178 insertions(+), 14 deletions(-) create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-02-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 0a583c0..710d36f 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -55,7 +55,7 @@ Remaining open work (from `FIX-PLAN.md`), batched toward the first `v2.0.0-beta` - [x] **API-01**: Make `SettingsHolder`/`ISettingsHolder` internal (A5, breaking) - [x] **PKG-01**: `Core.AspNet` exposes a public type or the package is dropped (A3) - [x] **PKG-02**: Float `Microsoft.Extensions.*` floor per-TFM (`8.0.x` for net8) or justify the pin (A4) -- [ ] **SRC-02**: Command-line binder parses quoted values with spaces correctly and skips `arg[0]` (A6) +- [x] **SRC-02**: Command-line binder parses quoted values with spaces correctly and skips `arg[0]` (A6) ### AOT/Trim & Documentation @@ -103,7 +103,7 @@ Deferred / held. Tracked but not in the current roadmap. | API-01 | Phase 3 | Complete | | PKG-01 | Phase 3 | Complete | | PKG-02 | Phase 3 | Complete | -| SRC-02 | Phase 3 | Pending | +| SRC-02 | Phase 3 | Complete | | AOT-01 | Phase 4 | Pending | | DOC-01 | Phase 4 | Pending | | REL-01 | Phase 5 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index e7bc779..9125197 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -23,7 +23,7 @@ value: config → typed settings maps accurately, and never leaks a secret doing - [x] **Phase 1: Exception Safety & Public Hierarchy** — ✓ COMPLETE (S1 #27, C2 #28 merged 2026-07-14) - No secret leaks; one catchable, structured `SimpleSettingsException` base - [x] **Phase 2: Binding Correctness & Engine Test Hardening** - Collections/nullable/converters verified; generator race closed by tests *(ENG-01/T7 done #29; COLL-01 + TEST-01/02/03 remain)* (completed 2026-07-14) -- [ ] **Phase 3: Public Surface, Packaging & Binder Cleanup** - Meaningful public surface; per-TFM deps; correct command-line parsing +- [x] **Phase 3: Public Surface, Packaging & Binder Cleanup** - Meaningful public surface; per-TFM deps; correct command-line parsing (completed 2026-07-14) - [ ] **Phase 4: AOT/Trim Honesty & Documentation** - Honest AOT/trim signals; canonically-named docs - [ ] **Phase 5: First v2.0.0-beta Release** - Batched breaking changes ship as an installable pre-release @@ -76,14 +76,14 @@ value: config → typed settings maps accurately, and never leaks a secret doing 3. A net8 consumer is no longer transitively forced onto `Microsoft.Extensions.* 10.x` (per-TFM floor), or the pin is documented with justification. 4. A quoted command-line value containing spaces binds correctly and the executable path (`arg[0]`) is skipped. -**Plans**: 1/2 plans executed +**Plans**: 2/2 plans complete **Wave 1** - [x] 03-01-PLAN.md — Public surface & packaging cleanup: SettingsHolder → internal sealed (API-01), drop dead Core.AspNet package (PKG-01), per-TFM Microsoft.Extensions.* floor (PKG-02) [Wave 1] **Wave 2** *(blocked on Wave 1 completion)* -- [ ] 03-02-PLAN.md — Command-line binder cleanup: SkipFirstArgument option + space-separated `--k v` lookahead + arg[0] skip + AddCommandLine tokenization (SRC-02) [Wave 2, depends on 03-01] +- [x] 03-02-PLAN.md — Command-line binder cleanup: SkipFirstArgument option + space-separated `--k v` lookahead + arg[0] skip + AddCommandLine tokenization (SRC-02) [Wave 2, depends on 03-01] ### Phase 4: AOT/Trim Honesty & Documentation @@ -121,6 +121,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 |-------|----------------|--------|-----------| | 1. Exception Safety & Public Hierarchy | n/a (shipped) | ✓ Complete | 2026-07-14 (#27/#28) | | 2. Binding Correctness & Engine Test Hardening | 2/2 | Complete | 2026-07-14 | -| 3. Public Surface, Packaging & Binder Cleanup | 1/2 | In Progress| | +| 3. Public Surface, Packaging & Binder Cleanup | 2/2 | Complete | 2026-07-14 | | 4. AOT/Trim Honesty & Documentation | 0/TBD | Not started | - | | 5. First v2.0.0-beta Release | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index a79b9fc..30b4d17 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,17 +4,17 @@ milestone: v2.0.0 milestone_name: milestone current_phase: 03 current_phase_name: public-surface-packaging-binder-cleanup -status: executing +status: verifying stopped_at: Phase 3 context gathered -last_updated: "2026-07-14T12:44:42.924Z" +last_updated: "2026-07-14T12:55:57.172Z" last_activity: 2026-07-14 last_activity_desc: Phase 03 execution started progress: total_phases: 5 - completed_phases: 1 + completed_phases: 2 total_plans: 4 - completed_plans: 3 - percent: 20 + completed_plans: 4 + percent: 40 --- # Project State @@ -30,10 +30,10 @@ See: .planning/PROJECT.md (updated 2026-07-13) Phase: 03 (public-surface-packaging-binder-cleanup) — EXECUTING Plan: 2 of 2 -Status: Ready to execute +Status: Phase complete — ready for verification Last activity: 2026-07-14 — Phase 03 execution started -Progress: [██░░░░░░░░] 20% +Progress: [████░░░░░░] 40% ## Performance Metrics @@ -58,6 +58,7 @@ Progress: [██░░░░░░░░] 20% | Phase 02 P01 | 2min | 3 tasks | 2 files | | Phase 02 P02 | 3min | 1 tasks | 1 files | | Phase 03 P01 | 3min | 3 tasks | 4 files | +| Phase 03 P02 | 4min | 2 tasks | 4 files | ## Accumulated Context @@ -75,6 +76,7 @@ Recent decisions affecting current work: - [Phase 02]: ENG-01 verified (not re-implemented): SettingsClassGenerator _generationGate + both concurrency stress tests green — Phase 2 success criterion #4 met by pre-GSD #29. - [Phase 02]: TUnit invocation uses --treenode-filter (Microsoft.Testing.Platform); legacy --filter returns zero tests / exit 5. - [Phase 02]: TEST-03 (Plan 02) scalar Uri/DateTime conversion locked via ScalarConversionTests (Uri->new Uri, DateTime->ParseExact yyyy-MM-dd, one format-mismatch negative asserting exception type only); no array-of-* or redaction duplication. Phase 2 success criterion #5 scalar coverage met on net10 (net8 via CI). +- [Phase 03]: SRC-02 (Plan 02): CLI binder lookahead + split-by-entry-point exe skip. SkipFirstArgument default false (AddArguments binds arg[0]); AddCommandLine sources GetCommandLineArgs() and sets SkipFirstArgument=true internally. Prefixed next-token = new key (diverges from Microsoft). Empty-safe zero-alloc prefix detection; CLI-path secret redaction (S1) held via store-only BindPropertySettings + regression test. ### Pending Todos @@ -96,6 +98,6 @@ Items acknowledged and carried forward: ## Session Continuity -Last session: 2026-07-14T12:44:37.068Z +Last session: 2026-07-14T12:55:01.741Z Stopped at: Phase 3 context gathered Resume file: .planning/phases/03-public-surface-packaging-binder-cleanup/03-CONTEXT.md diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-SUMMARY.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-SUMMARY.md new file mode 100644 index 0000000..e790b52 --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-02-SUMMARY.md @@ -0,0 +1,162 @@ +--- +phase: 03-public-surface-packaging-binder-cleanup +plan: 02 +subsystem: binders +tags: [command-line, cli, argument-parsing, lookahead, secret-redaction] + +# Dependency graph +requires: + - phase: 03-01 + provides: finalized package floors (PKG-02) and Core.AspNet-free graph (PKG-01) — stable restore/build graph +provides: + - CommandLineSettingsBinder space-separated (--k v) parsing via single-token lookahead + - SkipFirstArgument option (default false) split-by-entry-point exe-skip (D-05 refined) + - AddCommandLine sourcing Environment.GetCommandLineArgs() so quoted-with-spaces values bind end-to-end + - empty-safe zero-alloc CLI prefix detection (no IndexOutOfRangeException) + - CLI-path secret-redaction regression coverage (S2) +affects: [ship, verify-work, public-surface docs] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "Split-by-entry-point exe skip: AddArguments binds exactly, AddCommandLine owns the exe drop" + - "Single-token lookahead where a prefixed next-token is a new key (diverges from Microsoft's unconditional consume)" + +key-files: + created: [] + modified: + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs + - src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs + - src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs + +key-decisions: + - "SkipFirstArgument defaults false; AddArguments binds arg[0], AddCommandLine sets it true internally (D-05 refined, owner-approved)" + - "Prefix detection uses length-delta after TrimStart + Array.IndexOf on a cached char[] — empty-safe and zero-alloc, never bare token[0]" + - "A space-separated value beginning with a prefix char (-, /) is treated as a new key and does not bind; use the inline delimiter form (documented on ArgumentPrefixes)" + +patterns-established: + - "Lookahead consumes the next token as value only when the current token was prefixed and the next token is non-prefixed; a prefixed next token leaves the current key valueless (unstored)" + - "AddCommandLine wraps the caller action so SkipFirstArgument=true is applied first, then caller override runs" + +requirements-completed: [SRC-02] + +coverage: + - id: D1 + description: "Space-separated --k v and quoted-with-spaces single token bind to the full value (Phase SC #4)" + requirement: SRC-02 + verification: + - kind: unit + ref: "src/Tests/.../Binders/CommandLine/ArgumentsTests.cs#Build_WhenSpaceSeparatedValueHasSpaces_BindsFullValue" + status: pass + human_judgment: false + - id: D2 + description: "SkipFirstArgument default false binds arg[0]; true skips it (split-by-entry-point exe skip, D-05)" + requirement: SRC-02 + verification: + - kind: unit + ref: "src/Tests/.../Binders/CommandLine/ArgumentsTests.cs#Build_WhenSkipFirstArgumentTrue_SkipsIndexZero" + status: pass + human_judgment: false + - id: D3 + description: "Prefixed next-token is a new key; prefixed space-separated value does not bind (D-04/A4)" + requirement: SRC-02 + verification: + - kind: unit + ref: "src/Tests/.../Binders/CommandLine/ArgumentsTests.cs#Build_WhenNextTokenIsPrefixed_KeyIsNotStored, #Build_WhenSpaceSeparatedValueIsPrefixed_DoesNotBind" + status: pass + human_judgment: false + - id: D4 + description: "Empty-string token parsed safely with no IndexOutOfRangeException (A3/P1)" + requirement: SRC-02 + verification: + - kind: unit + ref: "src/Tests/.../Binders/CommandLine/ArgumentsTests.cs#Build_WhenEmptyToken_ParsesSafelyAndBindsRest" + status: pass + human_judgment: false + - id: D5 + description: "CLI-path unconvertible value never leaks: exception ToString() chain excludes the value while naming Age + Int32 (SEC-01/S1/S2)" + requirement: SRC-02 + verification: + - kind: unit + ref: "src/Tests/.../Binders/CommandLine/ArgumentsTests.cs#Build_WhenCliValueUnconvertible_ExceptionExcludesValue" + status: pass + human_judgment: false + - id: D6 + description: "AddCommandLine sources Environment.GetCommandLineArgs() and enables SkipFirstArgument=true (Open Q #1 resolved, in-scope)" + requirement: SRC-02 + verification: + - kind: automated + ref: "grep Environment.GetCommandLineArgs() && grep 'SkipFirstArgument = true' in SettingsBuilderFactoryExtensions.cs + Release build" + status: pass + human_judgment: false + +# Metrics +duration: 4min +completed: 2026-07-14 +status: complete +--- + +# Phase 3 Plan 2: Command-Line Binder Cleanup Summary + +**Command-line binder now parses real-world argv — space-separated `--k v` and quoted-with-spaces values bind via single-token lookahead, the exe is dropped only at the `AddCommandLine()` entry point (SkipFirstArgument default false), and prefix detection is empty-safe and zero-alloc.** + +## Performance + +- **Duration:** ~4 min +- **Started:** 2026-07-14T12:50:07Z +- **Completed:** 2026-07-14T12:53:49Z +- **Tasks:** 2 completed +- **Files modified:** 4 + +## Accomplishments +- Rewrote `CommandLineSettingsBinder.Parse` from a plain foreach into an index/lookahead loop: inline `--k=v`/`--k:v` preserved, new space-separated `--k v` support, a prefixed next-token treated as a new key (SimpleSettings deliberately diverges from Microsoft's unconditional consume). +- Added `SkipFirstArgument` (default **false**) implementing the refined split-by-entry-point exe skip: `AddArguments(Main(string[]))` binds exactly what it is handed; `AddCommandLine()` enables the skip internally. +- Switched `AddCommandLine` from `Environment.CommandLine.Trim().Split(' ')` to `Environment.GetCommandLineArgs()`, so a quoted value with spaces binds end-to-end unconditionally (Open Q #1 resolved, in-scope). +- Prefix detection made empty-safe and zero-alloc (length-delta after `TrimStart` for the current token; `Array.IndexOf` over a cached prefix `char[]` for the next token) — no bare `token[0]`, no per-token allocation. +- Added TUnit coverage incl. quoted-with-spaces headline, prefixed-next=new-key, prefixed-value non-binding, empty-token no-crash, explicit SkipFirstArgument true/false, and a CLI-path secret-redaction regression (S2). + +## Task Commits + +Each task was committed atomically: + +1. **Task 1 (RED): failing CLI binder cases** - `a9558fb` (test) +2. **Task 1 (GREEN): SkipFirstArgument option + lookahead Parse** - `29f5469` (feat) +3. **Task 2: AddCommandLine uses GetCommandLineArgs + owns exe skip** - `ac537cf` (feat) + +_Note: Task 1 is TDD — RED (test) then GREEN (feat). No refactor commit was needed._ + +## Files Created/Modified +- `src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs` - Added `SkipFirstArgument` (default false) + XML-doc note on `ArgumentPrefixes` (prefixed space-separated value is a new key, not a value). +- `src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs` - `Parse` rewritten to an index/lookahead loop with optional index-0 skip and empty-safe zero-alloc prefix detection; `SplitByDelimiter` unchanged; `BindPropertySettings` untouched (store-only, S1). +- `src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs` - `AddCommandLine` sources `Environment.GetCommandLineArgs()` and wraps the caller action to enable `SkipFirstArgument=true` (exe skip owned by this entry point, caller-overridable); `AddArguments` and signatures unchanged. +- `src/Tests/ExistForAll.SimpleSettings.UnitTests/Binders/CommandLine/ArgumentsTests.cs` - 10 new `[Test]` cases; class name kept `ArgumentsTests` for the treenode-filter; existing leading-space fixture unchanged. + +## Decisions Made +- Kept the exe skip split by entry point (D-05 refined) rather than a shared default-true — prevents silently dropping a real first argument for `AddArguments` callers. +- Prefix detection reuses the already-computed `name`/`key` length delta for the current token (no second scan) and `Array.IndexOf` over a cached `char[]` for the next token — allocation-free and empty-safe. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered +None. RED build failed to compile as expected (missing `SkipFirstArgument`), which is the intended RED gate; GREEN implementation turned all 26 `ArgumentsTests` green and the full 208-test suite passes on the local net10 runtime (both TFM DLLs), 0 warnings / 0 errors on the CI-flavored Release build. + +## User Setup Required + +None - no external service configuration required. + +## Verification Evidence + +- Targeted: `dotnet test SimpleSettings.slnx -c Release --treenode-filter "/*/*/ArgumentsTests/*"` — 26 passed, 0 failed. +- Plan-completion gate (both TFMs, CI-flavored): `dotnet restore` up-to-date; `dotnet build -c Release --no-restore -p:ContinuousIntegrationBuild=true` — Build succeeded, 0 Warning(s), 0 Error(s); `dotnet test -c Release --no-build` — **208 passed, 0 failed, 0 skipped**. +- Task 2 grep gate: `Environment.GetCommandLineArgs()` and `SkipFirstArgument = true` both present in `SettingsBuilderFactoryExtensions.cs`. + +## Threat Model Discharge +- T-03-04 (Tampering): values are stored as strings only; lookahead adds no injection sink; the empty-safe prefix detection removes the reachable `IndexOutOfRangeException` (A3). Mitigated. +- T-03-05 (Info Disclosure — exe path): accepted; `AddCommandLine` drops the exe at [0] via internally-enabled `SkipFirstArgument`. +- T-03-06 (Info Disclosure — secret redaction, S1): the rewritten `Parse` runs at construction/config-time outside the `ValuesPopulator` try/catch; `BindPropertySettings` stays store-only and untouched; CLI-path regression test (D5) asserts the unconvertible sentinel is absent from the thrown exception's full `ToString()` chain while `Age` + `Int32` remain. Mitigated. + +## Self-Check: PASSED From 4a8c8decc4a4d2163471f1716b24e3b10e590f4b Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 16:07:27 +0300 Subject: [PATCH 17/21] fix(03-02): null-guard CLI lookahead value token (post-review hardening) --- .../CommandLineSettingsBinder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs index 8ca34f0..bc001a9 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs @@ -61,6 +61,7 @@ private void Parse(string[] args) if (!tokenWasPrefixed || index + 1 >= args.Length) continue; var next = args[index + 1]; + if (next is null) continue; if (next.Length > 0 && Array.IndexOf(prefixes, next[0]) >= 0) continue; _argumentStore[name] = next; From 9a1f791a8468f24da24eff6a1bd4eb016ecda574 Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 16:07:48 +0300 Subject: [PATCH 18/21] docs(phase-03): complete phase execution (verified 4/4, review clean) --- .planning/ROADMAP.md | 2 +- .planning/STATE.md | 17 ++-- .../03-VERIFICATION.md | 91 +++++++++++++++++++ 3 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 .planning/phases/03-public-surface-packaging-binder-cleanup/03-VERIFICATION.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 9125197..374a2cd 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -121,6 +121,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 |-------|----------------|--------|-----------| | 1. Exception Safety & Public Hierarchy | n/a (shipped) | ✓ Complete | 2026-07-14 (#27/#28) | | 2. Binding Correctness & Engine Test Hardening | 2/2 | Complete | 2026-07-14 | -| 3. Public Surface, Packaging & Binder Cleanup | 2/2 | Complete | 2026-07-14 | +| 3. Public Surface, Packaging & Binder Cleanup | 2/2 | Complete | 2026-07-14 | | 4. AOT/Trim Honesty & Documentation | 0/TBD | Not started | - | | 5. First v2.0.0-beta Release | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 30b4d17..173183d 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,13 +2,13 @@ gsd_state_version: 1.0 milestone: v2.0.0 milestone_name: milestone -current_phase: 03 -current_phase_name: public-surface-packaging-binder-cleanup +current_phase: 4 +current_phase_name: AOT/Trim Honesty & Documentation status: verifying stopped_at: Phase 3 context gathered -last_updated: "2026-07-14T12:55:57.172Z" +last_updated: "2026-07-14T13:07:28.003Z" last_activity: 2026-07-14 -last_activity_desc: Phase 03 execution started +last_activity_desc: Phase 03 complete, transitioned to Phase 4 progress: total_phases: 5 completed_phases: 2 @@ -28,10 +28,10 @@ See: .planning/PROJECT.md (updated 2026-07-13) ## Current Position -Phase: 03 (public-surface-packaging-binder-cleanup) — EXECUTING -Plan: 2 of 2 +Phase: 4 — AOT/Trim Honesty & Documentation +Plan: Not started Status: Phase complete — ready for verification -Last activity: 2026-07-14 — Phase 03 execution started +Last activity: 2026-07-14 — Phase 03 complete, transitioned to Phase 4 Progress: [████░░░░░░] 40% @@ -39,7 +39,7 @@ Progress: [████░░░░░░] 40% **Velocity:** -- Total plans completed: 2 +- Total plans completed: 4 - Average duration: — - Total execution time: 0 hours @@ -48,6 +48,7 @@ Progress: [████░░░░░░] 40% | Phase | Plans | Total | Avg/Plan | |-------|-------|-------|----------| | 02 | 2 | - | - | +| 03 | 2 | - | - | **Recent Trend:** diff --git a/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VERIFICATION.md b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VERIFICATION.md new file mode 100644 index 0000000..134364d --- /dev/null +++ b/.planning/phases/03-public-surface-packaging-binder-cleanup/03-VERIFICATION.md @@ -0,0 +1,91 @@ +--- +phase: 03-public-surface-packaging-binder-cleanup +verified: 2026-07-14T13:20:00Z +status: passed +score: 4/4 must-haves verified +behavior_unverified: 0 +overrides_applied: 0 +--- + +# Phase 3: Public Surface, Packaging & Binder Cleanup Verification Report + +**Phase Goal:** The public API and packages carry only meaningful, correctly-scoped surface, and the command-line binder parses real-world arguments correctly — the remaining breaking changes batched before beta. +**Verified:** 2026-07-14T13:20:00Z +**Status:** passed +**Re-verification:** No — initial verification + +## Goal Achievement + +### Observable Truths + +| # | Truth (SC / REQ) | Status | Evidence | +| --- | ---------------- | ------ | -------- | +| 1 | API-01 / SC #1 — `SettingsHolder` is `internal sealed`, off the public surface, reachable from tests only via InternalsVisibleTo; suite green | ✓ VERIFIED | `SettingsHolder.cs:5` = `internal sealed class SettingsHolder : ISettingsHolder`; `ISettingsHolder.cs:5` = `internal interface`; `Info.cs` grants InternalsVisibleTo to UnitTests + Benchmark; only `public` match is the ctor of the internal type (not public surface); full suite 208/208 green (tests reach the type) | +| 2 | PKG-01 / SC #2 — `Core.AspNet` directory removed; no reference in slnx or UnitTests csproj; solution builds without it; test-local duplicate remains | ✓ VERIFIED | `Core/ExistForAll.SimpleSettings.Core.AspNet` DIR-ABSENT; `grep Core.AspNet` over slnx + UnitTests.csproj = NO-MATCHES; `Tests/.../Core/AspNet/Environments.cs` still present; CI Release build 0 errors both TFMs | +| 3 | PKG-02 / SC #3 — per-TFM `Microsoft.Extensions.*` floors resolve net8→8.0.x, net10→10.0.9 in the packable Binders/GenericHost assets; restore emits no NU1901–NU1904 | ✓ VERIFIED | assets floor check: Binders Configuration net8=8.0.0/net10=10.0.9, GenericHost DI.Abstractions net8=8.0.2/net10=10.0.9, `floor-check-errors: []` (guard passed); `dotnet restore -p:NuGetAuditMode=all` NO-ADVISORY; build `-p:TreatWarningsAsErrors=true` 0 warnings both TFMs | +| 4 | SRC-02 / SC #4 — CLI binder parses real-world args: space-separated `--key value`, quoted-value-with-spaces, prefixed-next-token=new key, SkipFirstArgument default false + AddArguments binds arg[0], AddCommandLine sources GetCommandLineArgs() and skips exe, empty-token safe, CLI-path redaction asserts no value leak | ✓ VERIFIED | 26 ArgumentsTests pass on both TFM DLLs incl. `Build_WhenSpaceSeparatedValueHasSpaces_BindsFullValue`, `Build_WhenNextTokenIsPrefixed_KeyIsNotStored`, `Build_WhenSkipFirstArgumentTrue_SkipsIndexZero`, `Build_WhenEmptyToken_ParsesSafelyAndBindsRest`, `Build_WhenSpaceSeparatedValueIsPrefixed_DoesNotBind`, `Build_WhenCliValueUnconvertible_ExceptionExcludesValue`; source confirms `SkipFirstArgument=false` default, empty-safe zero-alloc prefix detection (`Array.IndexOf`, `name.Length != key.Length`), `AddCommandLine` uses `Environment.GetCommandLineArgs()` + `SkipFirstArgument = true` | + +**Score:** 4/4 truths verified (0 present, behavior-unverified) + +All four truths are behavior-dependent (parsing state transitions, per-TFM restore resolution) and each is backed by a passing behavioral test or a live restore+assets inspection run in this verification — none passed on symbol presence alone. + +### Required Artifacts + +| Artifact | Expected | Status | Details | +| -------- | -------- | ------ | ------- | +| `src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs` | internal sealed class | ✓ VERIFIED | Line 5 exact match; consumed by internal `SettingsCollection`; wired via suite | +| `src/Directory.Packages.props` | per-TFM conditional PackageVersion items | ✓ VERIFIED | Two mutually-exclusive `` blocks, one item per package per group, no unconditional twin (NU1504-safe) | +| `src/SimpleSettings.slnx` | no Core.AspNet Project entry | ✓ VERIFIED | Only core/Binders/GenericHost/UnitTests/Benchmark entries remain | +| `src/Tests/.../ExistForAll.SimpleSettings.UnitTests.csproj` | no Core.AspNet ProjectReference | ✓ VERIFIED | Three ProjectReferences (Binders, core, GenericHost); no Core.AspNet | +| `CommandLineSettingsBinderOptions.cs` | `public bool SkipFirstArgument { get; set; } = false;` | ✓ VERIFIED | Line 26 exact; ArgumentPrefixes carries the A4 XML-doc note | +| `CommandLineSettingsBinder.cs` | lookahead Parse, index-0 skip, empty-safe zero-alloc prefix detection | ✓ VERIFIED | Index loop lines 46-68; cached `prefixes` array; `Array.IndexOf(prefixes, next[0])`; no bare `token[0]`; SplitByDelimiter unchanged; BindPropertySettings store-only | +| `SettingsBuilderFactoryExtensions.cs` | AddCommandLine uses GetCommandLineArgs() + SkipFirstArgument=true | ✓ VERIFIED | Lines 44-50; wraps caller action so skip applied first then caller override | +| `ArgumentsTests.cs` | new TUnit cases for all SRC-02 behaviors | ✓ VERIFIED | 13 methods incl. all headline cases; class name `ArgumentsTests` stable; leading-space fixture preserved | + +### Key Link Verification + +| From | To | Via | Status | Details | +| ---- | -- | --- | ------ | ------- | +| `Info.cs` | UnitTests/Benchmark assemblies | InternalsVisibleTo | ✓ WIRED | Both assembly attributes present; internal SettingsHolder reachable — 208/208 suite green proves it | +| `Directory.Packages.props` | Binders/GenericHost assets | `$(TargetFramework)` conditional restore | ✓ WIRED | assets.json shows net8 8.0.x / net10 10.0.9 resolved per-TFM | +| `AddCommandLine` | `CommandLineSettingsBinder` | `GetCommandLineArgs()` → `AddArguments(args, o => o.SkipFirstArgument=true)` | ✓ WIRED | Exe-skip owned by entry point; verified in source + SkipFirstArgument=true test | +| `SkipFirstArgument` | `Parse` iteration start | `start = SkipFirstArgument && args.Length>0 ? 1 : 0` | ✓ WIRED | Line 44; true/false cases both pass | +| test-local `Core/AspNet/Environments.cs` | Core/AspNet tests | independent namespace `...UnitTests.Core.AspNet` | ✓ WIRED | Duplicate preserved; suite green after package removal | + +### Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +| ----------- | ----------- | ----------- | ------ | -------- | +| API-01 | 03-01 | Make `SettingsHolder`/`ISettingsHolder` internal | ✓ SATISFIED | Both types internal (SettingsHolder internal sealed, ISettingsHolder internal interface) | +| PKG-01 | 03-01 | `Core.AspNet` dropped | ✓ SATISFIED | Directory + all references removed; solution builds | +| PKG-02 | 03-01 | Float `Microsoft.Extensions.*` floor per-TFM | ✓ SATISFIED | net8→8.0.x, net10→10.0.9; no advisories | +| SRC-02 | 03-02 | CLI binder parses quoted-with-spaces values and skips arg[0] | ✓ SATISFIED | 26 ArgumentsTests pass | + +All four declared requirement IDs (API-01, PKG-01, PKG-02, SRC-02) are claimed by the plans and map exactly to the Phase 3 rows in REQUIREMENTS.md traceability. No orphaned requirements. REQUIREMENTS.md marks all four `[x]` Complete. + +### Behavioral Spot-Checks + +| Behavior | Command | Result | Status | +| -------- | ------- | ------ | ------ | +| Restore audit clean | `dotnet restore -p:NuGetAuditMode=all` | exit 0, NO-ADVISORY | ✓ PASS | +| CI build warnings-as-errors | `dotnet build -c Release --no-restore -p:ContinuousIntegrationBuild=true -p:TreatWarningsAsErrors=true` | Build succeeded, 0 Warning(s), 0 Error(s) | ✓ PASS | +| Per-TFM floor resolution | assets.json floor check (Binders + GenericHost) | `floor-check-errors: []` | ✓ PASS | +| Targeted CLI binder tests | `dotnet test --treenode-filter "/*/*/ArgumentsTests/*"` | 26 passed, 0 failed (both TFM DLLs) | ✓ PASS | +| Full suite | `dotnet test SimpleSettings.slnx -c Release --no-build` | 208 passed, 0 failed, 0 skipped (both TFM DLLs) | ✓ PASS | + +### Anti-Patterns Found + +None. No TODO/FIXME/XXX/HACK/PLACEHOLDER markers in any of the four modified source files. + +### Owner-Optional Follow-Up (Informational — NOT a phase gate) + +The published `ExistForAll.SimpleSettings.Core.AspNet` 2.0.0-alpha.0.* prereleases remain listed on NuGet.org after the in-repo removal. Unlisting them (`dotnet nuget delete ...` under the guy-lud account) is documented in 03-01 `user_setup` and coverage item D4, explicitly marked owner-optional and NOT required for phase completion (D-02b). This is external registry hygiene outside the codebase and does not gate the phase goal; recorded here for visibility, not as a blocking human-verification item. + +### Gaps Summary + +No gaps. All four phase success criteria are observably true in the codebase and confirmed by live build, restore-audit, per-TFM assets inspection, and a green 208-test suite across both target-framework DLLs. The public surface is trimmed (`SettingsHolder`/`ISettingsHolder` internal), the dead `Core.AspNet` package is fully removed while the test-local duplicate is preserved, per-TFM `Microsoft.Extensions.*` floors resolve correctly with no security advisories, and the command-line binder parses real-world arguments (space-separated, quoted-with-spaces, prefixed-next-token semantics, entry-point-scoped exe skip, empty-token safety) with a CLI-path secret-redaction regression proving no value leak. + +--- + +_Verified: 2026-07-14T13:20:00Z_ +_Verifier: Claude (gsd-verifier)_ From cf691972c76fb46333491a70463f4225b754d96f Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 16:07:48 +0300 Subject: [PATCH 19/21] docs(backlog): capture client pre-beta engine requirements (COLL/VAL/API) --- .../backlog/client-requirements-pre-beta.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .planning/backlog/client-requirements-pre-beta.md diff --git a/.planning/backlog/client-requirements-pre-beta.md b/.planning/backlog/client-requirements-pre-beta.md new file mode 100644 index 0000000..3b7f68d --- /dev/null +++ b/.planning/backlog/client-requirements-pre-beta.md @@ -0,0 +1,45 @@ +# Client requirements — pre-beta engine features (BACKLOG) + +**Source:** client (via owner), captured 2026-07-14 +**Status:** BACKLOG — to be routed into ROADMAP/REQUIREMENTS after Phase 3 completes. +**Intended window:** before the first `v2.0.0-beta` (this milestone batches breaking changes pre-beta). + +> Relationship to existing deferrals: **#1 and #2 pull `COLL-01` (List support) forward and expand it**; **#3 is the held `D1 Validations`**. Recommend inserting a dedicated engine phase before the beta and renumbering the beta phase last. Provisional requirement IDs proposed below — finalize during roadmap integration. + +--- + +## COLL-02 — Empty-collection default *(cheapest; pure win; no back-compat risk)* +- **What:** an unset `T[]` / `List` currently binds `null`; only `IEnumerable` binds empty. Make all three default to an **empty collection, never null**. +- **Where:** `TypeConverter.ConvertValue` null branch. Root cause: `IsEnumerable()` matches only the open `IEnumerable<>` typedef, so arrays / `List` fall through to `return null`. Return `Array.CreateInstance(elementType, 0)` / a new `List` for those. +- **Done when:** unset `T[]`, `List`, `IEnumerable` all bind empty. ~1–2 lines. +- **Unblocks:** gateway drops every `?? []` guard. + +## COLL-03 — YAML-sequence / indexed-child binding *(headline readability fix)* +- **What:** collections bind only from a comma-scalar (`"a,b,c"`) today; a readable YAML `- a` / `- b` sequence is silently dropped. Make the binder read **sequence children**. +- **Where:** `ConfigurationBinder.BindPropertySettings` reads a single scalar `section[key]` before conversion runs, so child sections (`key:0`, `key:1`, …) never reach a converter — a converter can't fix this, the **binder** must. Enumerate `GetChildren()` for collection targets. +- **Load-bearing (must keep working):** the comma-scalar form MUST still bind — prod env-var overrides (`MultiHost__CommonHosts`) depend on it. +- **Sub-rules:** children win if both a scalar and children exist; empty / whitespace / empty-sequence → empty (never `[""]`); each element still flows the inner converter chain (so `int[]` / enum arrays keep working); honor `[SettingsSection]` / root prefixing exactly as the scalar path. +- **Unblocks:** revert comma-scalar hacks back to readable sequences in the gateway (`PublicRoute:Routes`, `MultiHost:CommonHosts`, CORS). +- **⚠ Security note:** this changes `BindPropertySettings`, the redaction-critical method Phase 3 left untouched (runs inside the `SettingsBindingException` chaining catch). The S1/SEC-01 secret-redaction invariant MUST be re-verified when this lands. + +## VAL-01 — Working class-level / settings-object validation *(= held D1)* +- **What:** `SettingsPropertyAttribute.ValidatorType` and `ISettingValidation` are declared but **never invoked** anywhere in 1.0.0 — dead scaffolding. Wire them into the bind pipeline so a settings object can validate itself (incl. cross-property rules). +- **Where:** invoke the declared validator after conversion/population in the bind flow (today nothing calls it). +- **Unblocks:** gateway Story 8.8 — replace the interim FluentValidation `SpiceDbSettingsValidator` (the PR #103 boot-hook workaround) with the native mechanism. + +## VAL-02 — Tighter `AllowEmpty` *(lowest urgency — consumers already fail closed)* +- **What:** `[SettingsProperty(AllowEmpty=false)]` rejects only `null` at bind; an explicit `""`, whitespace, or an unsubstituted `${ENV:-}` placeholder binds clean. Reject those too. +- **Where:** `TypeConverter.ValidateNullAcceptance` (the current null-only check). Naturally rides on VAL-01 if expressed as an `ISettingValidation`. +- **Value:** converts a late runtime failure into a clean startup Fatal. Low urgency because every gateway consumer already denies-all on a blank secret. + +## API-02 — Expose `ISettingsCollection` from the DI extension +- **What:** the `AddSimpleSettings` DI extension method should, in the end, expose the `ISettingsCollection` somehow. +- **Where:** DI registration extension (`AddSimpleSettings`). +- **Note:** bundled with the client's item #4; scope/shape (return value vs. resolvable service) to be decided during planning. + +--- + +## Recommended integration (after Phase 3) +1. `/gsd-new-milestone` OR extend the current milestone: add these as REQUIREMENTS.md entries (finalize IDs). +2. Insert a **new engine phase** (collections + validation) before the beta; renumber: current Phase 4 (AOT/docs) and Phase 5 (beta) shift out so the beta stays last. +3. Discuss-phase for the new phase (COLL-02/03 sequencing, VAL-01 pipeline hook point, the S1 re-verification gate for COLL-03). From 4c9a51e0348bcee2ebb2db03142d30dab874846a Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 16:17:13 +0300 Subject: [PATCH 20/21] docs: refresh handoff + record approved pre-beta engine phase (client reqs) --- .../backlog/client-requirements-pre-beta.md | 12 ++- SESSION-HANDOFF.md | 92 ++++++++----------- 2 files changed, 48 insertions(+), 56 deletions(-) diff --git a/.planning/backlog/client-requirements-pre-beta.md b/.planning/backlog/client-requirements-pre-beta.md index 3b7f68d..3cd9952 100644 --- a/.planning/backlog/client-requirements-pre-beta.md +++ b/.planning/backlog/client-requirements-pre-beta.md @@ -39,7 +39,11 @@ --- -## Recommended integration (after Phase 3) -1. `/gsd-new-milestone` OR extend the current milestone: add these as REQUIREMENTS.md entries (finalize IDs). -2. Insert a **new engine phase** (collections + validation) before the beta; renumber: current Phase 4 (AOT/docs) and Phase 5 (beta) shift out so the beta stays last. -3. Discuss-phase for the new phase (COLL-02/03 sequencing, VAL-01 pipeline hook point, the S1 re-verification gate for COLL-03). +## Integration decision — APPROVED 2026-07-14 (owner) +**Chosen: new engine phase before the beta** (renumber, beta stays last). Phase 3 shipped separately as PR #31. + +1. Add these as `REQUIREMENTS.md` entries (finalize IDs COLL-02/COLL-03/VAL-01/VAL-02/API-02). +2. Insert as **new Phase 4 "Collection & Validation Binding"** via `/gsd-phase`; renumber current Phase 4 (AOT/docs) → **Phase 5**, current Phase 5 (beta) → **Phase 6**. +3. `/gsd-discuss-phase 4` → plan → execute. Discussion must cover: COLL-02/03 sequencing, VAL-01 pipeline hook point, the **S1 secret-redaction re-verification gate for COLL-03** (it edits `BindPropertySettings`), and the load-bearing comma-scalar compatibility. + +Staged on branch `gsd/phase-4-collection-validation-binding` (forked off the Phase 3 tip; rebase onto `master` after PR #31 merges). diff --git a/SESSION-HANDOFF.md b/SESSION-HANDOFF.md index 6d2f830..73ecfff 100644 --- a/SESSION-HANDOFF.md +++ b/SESSION-HANDOFF.md @@ -4,48 +4,34 @@ _Last updated: 2026-07-14 · owner: Guy Ludvig (guy@frontegg.com)_ ## TL;DR **GSD is the source of truth** (`.planning/`); `FIX-PLAN.md` is frozen historical reference only. -Phases **1 & 2 are complete and merged** (PR #30 squash → `master` @ `67aa72f`). `master` is now @ **`8750359`** (a follow-up commit that tracks the `.claude/`/`.codex/` tooling + a handoff refresh — pushed, so it publishes a throwaway alpha; the "remove later" cleanup is still pending). +Phases **1 & 2 merged** (PR #30 → `master` @ `67aa72f`; `master` now @ **`8750359`**). **Phase 3 — Public Surface, Packaging & Binder Cleanup — is COMPLETE and in open PR #31** (`https://github.com/existall/SimpleSettings/pull/31`, base `master`, head `gsd/phase-3-public-surface-packaging-binder-cleanup` @ `cf69197`). Verified **4/4 must-haves**, review-clean, suite **208/208** (net8+net10), build 0 warnings. -**Active work: Phase 3 — Public Surface, Packaging & Binder Cleanup**, on feature branch **`gsd/phase-3-public-surface-packaging-binder-cleanup`** (forked off `master` @ `8750359`). **Discuss is DONE** (CONTEXT + DISCUSSION-LOG committed) and **research is DONE** (`03-RESEARCH.md`, committed `d8e7a89`, HIGH confidence). **Planning resumes at the planner step** — re-run `/gsd-plan-phase 3` and it auto-uses the existing research. Suite **94 tests net10** (CI net8 + net10). +**Next milestone work is decided:** a **new engine phase goes in BEFORE the beta** to satisfy client requirements (approved 2026-07-14). Currently on a LOCAL staging branch **`gsd/phase-4-collection-validation-binding`** (forked off the Phase 3 tip `cf69197`, NOT pushed). The roadmap has NOT been restructured yet. ## Do this first (new session) -1. **Verify git state** (`git log -3`, `git branch --show-current`, `gh pr list`): expect to be on **`gsd/phase-3-…`**, `master` @ `8750359` (pushed), PR #30 merged, no open PRs. Working tree may carry an uncommitted handoff refresh + untracked build noise. -2. **Check Phase 3 planning progress:** `ls .planning/phases/03-public-surface-packaging-binder-cleanup/` - - `03-CONTEXT.md` + `03-DISCUSSION-LOG.md` are committed (discuss done). - - **If `03-RESEARCH.md` exists and NO `03-*-PLAN.md` yet** → resume planning: **`/gsd-plan-phase 3`** — it auto-uses the existing RESEARCH.md (no re-prompt) and continues: VALIDATION.md → pattern-mapper → gsd-planner → gsd-plan-checker (revision loop, max 3) → coverage gates → PLANNED. - - **If `03-*-PLAN.md` files exist** → planning finished; run **`/gsd-execute-phase 3`**. -3. **Stay on the feature branch.** Ship Phase 3 as one PR to `master` via `guy-lud` after it verifies — never commit docs directly to `master` (burns an alpha). - -## Phase 3 — locked decisions (from discuss; full detail + file:line refs in `03-CONTEXT.md`) -- **API-01:** `SettingsHolder` (`SettingsHolder.cs:5`) `public class` → **`internal sealed`**. `ISettingsHolder` already internal; only `SettingsCollection` uses it; tests keep access via `InternalsVisibleTo` (`Info.cs:3-4`). -- **PKG-01:** **DROP the `Core.AspNet` package** (its only type is `internal static Environments` → zero public surface). Remove `SimpleSettings.slnx:13` entry + `UnitTests.csproj:19` ProjectReference; confirm no test uses `Environments`. -- **PKG-02:** **Float `Microsoft.Extensions.*` per-TFM** in `Directory.Packages.props:8-12` (all four): net8 → **latest `8.0.x`**, net10 → `10.0.x`, via CPM conditional `PackageVersion`. -- **SRC-02:** CLI binder (`CommandLineSettingsBinder.cs`): **add space-separated `--key value`** (lookahead; a prefixed next token = new key) + keep inline `=`/`:`; **skip `arg[0]` by default** via a new `SkipFirstArgument` option (default true) on `CommandLineSettingsBinderOptions`. - -## Phase 3 — verified research findings (feed the planner; full detail in `03-RESEARCH.md`) -- **PKG-02 (verified by live restore, 0 warnings, no NU1504):** per-TFM conditional `` works. **Exact net8 floors differ per package:** `Configuration`=`8.0.0`, `Configuration.Json`=`8.0.1`, `DependencyInjection`=`8.0.1`, `DependencyInjection.Abstractions`=`8.0.2`. net10 stays `10.0.9`. `CentralPackageTransitivePinningEnabled` stays off. -- **PKG-01:** `Core.AspNet` **is published on NuGet** (`1.0.0` + a `2.0.0-alpha.0.N` run) → pair removal with **unlisting** the alphas (owner choice, low-stakes). The `UnitTests.csproj:19` ProjectReference is **DEAD** (the package's `Environments` is internal w/ no `InternalsVisibleTo`; tests use a local duplicate) → safe to remove. -- **API-01:** flip is self-contained — no test references `SettingsHolder`; no PublicAPI analyzer/baseline in the repo. -- **SRC-02:** our lookahead **deliberately diverges** from Microsoft's provider (which consumes the next token unconditionally). The `arg[0]` skip is needed because `AddCommandLine()` uses `Environment.CommandLine.Split(' ')` (includes exe path + quote-unsafe). Existing `ArgumentsTests` stay green under `SkipFirstArgument=true` (fixture's leading space makes `args[0]` empty). -- **Open questions for planning:** - 1. **(recommend YES, in-scope)** Switch `AddCommandLine()` from `Environment.CommandLine.Split(' ')` → `Environment.GetCommandLineArgs()` so the "quoted value with spaces" criterion holds end-to-end for that entry point. - 2. **(resolved)** net8 floor = latest-patch-per-package (above), not a uniform `8.0.0`. - -## Current state -- On **`gsd/phase-3-public-surface-packaging-binder-cleanup`**. Phase-3 commits so far: `e945d99` (03-CONTEXT + 03-DISCUSSION-LOG), `6cbf370` (STATE record-session). Research step ran after that. -- **Phases 1–2 merged.** Build clean (0 warnings, both TFMs). Suite **94 net10**. -- The merged branch `chore/gsd-ownership-cutover` is deleted locally but **still exists on `origin`** (optional cleanup via `guy-lud`). -- A **`gh-pages`** branch holds the benchmark baseline (`dev/bench/`) — do **not** delete it. - -## What shipped in Phases 1–2 (merged via PR #30) -- **Phase 1** — S1 secret redaction (#27), C2 public exception hierarchy (#28): `SettingsPropertyValueException` carries no value / chains no inner; all exceptions derive from `public abstract SimpleSettingsException`. -- **Phase 2** — engine test hardening (test-only): `ValuesPopulatorTests` (TEST-01 precedence/default), `TypeConverterTests` (TEST-02 null/nullable/ConverterType), `ScalarConversionTests` (TEST-03 Uri/DateTime), ENG-01 verify-only (generator concurrency gate from #29 confirmed; `..._IsRaceFree` is the load-bearing proof). COLL-01 owner-deferred. - -## Remaining roadmap (5 phases; 1–2 done) -- **Phase 3 (active):** API-01, PKG-01, PKG-02, SRC-02 — above. -- **Phase 4:** AOT-01 (reflection/AOT-trim annotations), DOC-01 (README refresh). -- **Phase 5:** REL-01 (cut the first `v2.0.0-beta`; suite green net8 + net10). -- **Owner-deferred / held:** COLL-01 (`List` support), D1 Validations, D2 EqualityCompererCreator. +1. **Verify git state** (`git log -3`, `git branch --show-current`, `gh pr list`): expect current branch **`gsd/phase-4-collection-validation-binding`** (local), PR **#31 open** (Phase 3), PR #30 merged. `master` @ `8750359`. +2. **Decide the Phase-3 PR #31 disposition:** merge it to `master` via `guy-lud` (publishes a throwaway alpha — expected pre-stable), or leave open while stacking Phase 4. If #31 merges, **rebase `gsd/phase-4-collection-validation-binding` onto updated `master`** (it's currently stacked on the unmerged Phase 3 tip). +3. **Formalize the new engine phase** (client reqs — full detail in `.planning/backlog/client-requirements-pre-beta.md`): + - Add 5 requirements to `REQUIREMENTS.md`: **COLL-02** (empty-collection default), **COLL-03** (YAML-sequence / indexed-child binding), **VAL-01** (wire `ISettingValidation`/`ValidatorType`), **VAL-02** (tighter `AllowEmpty`), **API-02** (`AddSimpleSettings` exposes `ISettingsCollection`). + - Insert them as **new Phase 4 "Collection & Validation Binding"** via `/gsd-phase` (use it for safe renumbering): current **Phase 4 (AOT/Trim & Docs) → Phase 5**, current **Phase 5 (beta) → Phase 6**. Beta stays last. + - Then **`/gsd-discuss-phase 4`** → plan → execute (fresh session per phase; `/clear` between). + +## Phase 3 — SHIPPED (in PR #31, awaiting merge) +- **API-01** `SettingsHolder` → `internal sealed`. **PKG-01** `Core.AspNet` package removed (+ dead test ref). **PKG-02** per-TFM `Microsoft.Extensions.*` floors (net8: `Configuration` 8.0.0, `DependencyInjection.Abstractions` 8.0.2; net10 10.0.9) + restore-time NU1901–1904 audit gate. **SRC-02** CLI binder: space-separated `--k v` + quoted-value binding, `SkipFirstArgument` (default **false**), `AddCommandLine()` → `GetCommandLineArgs()` owns exe-skip. +- **D-05 was refined mid-planning (owner-approved):** the exe-skip is split by entry point (NOT a shared default-true) — see `03-CONTEXT.md` D-05. +- **Owner follow-up (not automated):** unlist the published `Core.AspNet` `2.0.0-alpha.0.*` prereleases on NuGet.org (guy-lud account). + +## New engine phase — client requirements (APPROVED; to formalize) +Source detail + provisional IDs + file locations + load-bearing constraints: **`.planning/backlog/client-requirements-pre-beta.md`**. Summary: +- **COLL-02 / COLL-03** pull deferred **COLL-01** forward and expand it (empty-collection default; sequence binding). **VAL-01** is the held **D1 Validations**. +- **Load-bearing constraints to preserve when planning:** (a) comma-scalar collection binding MUST keep working (prod env-var overrides `MultiHost__CommonHosts` depend on it); (b) **COLL-03 changes `ConfigurationBinder.BindPropertySettings`** — the redaction-critical method Phase 3 deliberately left untouched — so the **S1/SEC-01 secret-redaction invariant MUST be re-verified** when it lands; (c) collection converter chain must still run per element (int[]/enum arrays). +- Engine files in scope (different from Phase 3's CLI files): `TypeConverter.ConvertValue` / `ValidateNullAcceptance`, `ConfigurationBinder.BindPropertySettings`, `SettingsPropertyAttribute`/`ISettingValidation`, the `AddSimpleSettings` DI extension. + +## Remaining roadmap (after the insert/renumber) +- **Phase 4 (NEW, engine):** COLL-02, COLL-03, VAL-01, VAL-02, API-02. +- **Phase 5 (was 4):** AOT-01 (reflection/AOT-trim annotations), DOC-01 (README refresh — MUST add the review's "spaced secrets now bind via `AddCommandLine`" migration note + the Phase-3 breaking-change list). +- **Phase 6 (was 5):** REL-01 (cut the first `v2.0.0-beta`; suite green net8 + net10). +- **Still held:** D2 EqualityComparerCreator. ## How releasing works (durable) - **`ci.yml`** — PRs to `master`: build + test (net8.0 + net10.0). **`release.yml`**: push to `master` → auto-publishes a MinVer height-based `-alpha` (NO `paths` filter — every push, incl. docs, publishes). Manual **Release** (`workflow_dispatch`, channel beta/rc/stable + bump) tags `v*`. @@ -53,22 +39,24 @@ Phases **1 & 2 are complete and merged** (PR #30 squash → `master` @ `67aa72f` - **Versioning = MinVer**, tag prefix `v`, baseline **2.0.0**, keyless NuGet Trusted Publishing (OIDC). Workflows use `SOLUTION=SimpleSettings.slnx`. **Everything goes through PRs.** ## Gotchas a new session MUST know -- **TUnit test filtering:** `dotnet test --filter "*Name*"` is **rejected** by Microsoft.Testing.Platform/TUnit — exits **5**, zero tests (looks green at a glance). Use `--treenode-filter "/*/*/ClassNameTests/*"` or run unfiltered. Several `*-PLAN.md` `` blocks still carry the wrong `--filter` form. See `[[simplesettings-test-stack]]`. -- **Run `dotnet` from `src/`** (global.json → Microsoft.Testing.Platform). net10 runtime only locally → net8 build-only locally; CI runs both. Don't `cd ` before `dotnet`. -- **Pushing / PRs:** active `git`/`gh` identity (`guy-frontegg`) is **read-only** here; push/PR/merge via **`guy-lud`**. `origin` uses SSH alias **`github-guy-lud`** → `git push` already uses guy-lud. For `gh` writes: `gh auth switch --user guy-lud`, then switch back to `guy-frontegg`. See `[[simplesettings-push-access]]`. -- **Never commit docs to `master`** (release.yml has no `paths` filter → burns an alpha). Wrap ritual: refresh THIS file so it rides the current work branch (currently committed on `gsd/phase-3-…`). See `[[simplesettings-handoff-workflow]]`. -- **GSD branching:** `config.json` `branching_strategy: none` → `/gsd-execute-phase` commits on the *current* branch. Keep Phase 3 on its feature branch; do NOT execute on `master`. See `[[simplesettings-gsd-source-of-truth]]`. -- **Review workflow (`[[dotnet-review-workflow]]`):** plan → review the plan with `dotnet-architect`/`performance-analyst`/`security-auditor` → implement → review finished code with `code-reviewer`. All four kit agents fired cleanly recently; still verify each returns real tool calls; `/code-review` skill is a reliable fallback. +- **TUnit test filtering:** `dotnet test --filter "*Name*"` is **rejected** by Microsoft.Testing.Platform/TUnit — exits **5**, zero tests (looks green at a glance). Use `--treenode-filter "/*/*/ClassNameTests/*"` or run unfiltered. See `[[simplesettings-test-stack]]`. +- **Run `dotnet` from `src/`** (global.json → Microsoft.Testing.Platform). net10 runtime only locally (both TFM test DLLs run on it); net8 runs natively in CI. Don't `cd ` before `dotnet`. +- **Pushing / PRs:** active `git`/`gh` identity (`guy-frontegg`) is **read-only** here; push/PR/merge via **`guy-lud`**. `origin` uses SSH alias **`github-guy-lud`** → `git push` already uses guy-lud. For `gh` writes: `gh auth switch --user guy-lud`, do the write, then `gh auth switch --user guy-frontegg`. See `[[simplesettings-push-access]]`. +- **Never commit docs to `master`** (release.yml has no `paths` filter → burns an alpha). Wrap ritual: refresh THIS file on the current work branch (now committed on `gsd/phase-4-…`). See `[[simplesettings-handoff-workflow]]`. +- **GSD branching:** `config.json` `branching_strategy: none` → `/gsd-execute-phase` commits on the *current* branch. Keep each phase on its own feature branch; never execute on `master`. See `[[simplesettings-gsd-source-of-truth]]`. +- **Review workflow (`[[dotnet-review-workflow]]`):** plan → review the plan with `dotnet-architect`/`performance-analyst`/`security-auditor` → implement → review finished code with `code-reviewer`. Phase 3 ran all of these; the architect caught a HIGH false-green (a vacuous verify), so **take the specialist reviews seriously — they earn their cost.** +- **VALIDATION.md gotcha (`[[gsd-plan-phase-validation-md]]`):** with Nyquist on, `/gsd-plan-phase` §5.5 writes only VALIDATION.md frontmatter → the plan-checker BLOCKS on the stub. Populate its body from RESEARCH's "## Validation Architecture" before the checker runs. - Commits/PRs here **omit** the Co-Authored-By / Generated-with trailer (project preference). ## Key decisions & context (carry forward) -- **Exception-redaction invariant (S1+C2, locked).** `SettingsPropertyValueException` never carries the bound value / never chains an inner; `SettingsBindingException` stores primitives; `SettingsPropertyNullException` = value-free "required missing". Don't weaken. -- **Generator concurrency (ENG-01/T7, #29).** One `_generationGate` over all generation (double-checked lock; warm path lock-free). Don't switch to `Lazy`-per-type (distinct-interface `DefineType` also races the shared `ModuleBuilder`). -- **Benchmark tracking gates on ALLOCATIONS, not time** (`gh-pages` baseline). -- **C3** = provider-level cache (option 2); reload/`IOptionsMonitor` is future "option 3". +- **Exception-redaction invariant (S1+C2, locked).** `SettingsPropertyValueException` never carries the bound value / never chains an inner; `SettingsBindingException` stores primitives; `SettingsPropertyNullException` = value-free "required missing". Don't weaken. **Re-verify for COLL-03 (touches `BindPropertySettings`).** +- **Generator concurrency (ENG-01/T7, #29).** One `_generationGate` over all generation (double-checked lock; warm path lock-free). Don't switch to `Lazy`-per-type. +- **Benchmark tracking gates on ALLOCATIONS, not time** (`gh-pages` baseline). The CLI binder is NOT in the benchmark set. - **Pre-stable window:** no `v*` tag; breaking changes free until the first `v2.0.0-beta`. ## Minor tracked follow-ups (non-blocking) -- **`.claude/`/`.codex/` are tracked on `master`** (commit `8750359`, "remove later"). Cleanup: add to `.gitignore` + `git rm --cached` — do it on a **branch/PR** (on `master` = another alpha). -- **Cosmetic NITs** (Phase 2 code review) in `Core/TypeConverterTests.cs`: add `using System;`; optionally rename `Convert_NullForNonNullableValueType_ReturnsTypeDefault` → `..._WithoutAttribute_...`. -- **REQUIREMENTS.md traceability:** 13 brownfield baseline IDs (`BIND-01…NAME-01`) are in the body but not the traceability table (pre-existing; surfaces in `/gsd-progress`). +- **`.claude/`/`.codex/` tracked on `master`** (`8750359`, "remove later"): add to `.gitignore` + `git rm --cached` on a **branch/PR** (never on `master`). +- **REQUIREMENTS.md traceability:** 13 brownfield baseline IDs (`BIND-01…NAME-01`) are in the body but not the traceability table (pre-existing; surfaces in `/gsd-progress` and on `phase.complete`). +- **Phase 2 cosmetic NITs** in `Core/TypeConverterTests.cs`: add `using System;`; optional test rename. +- **Stale remote branch** `origin/chore/gsd-ownership-cutover` (merged) — safe to delete via `guy-lud`. +- **Codebase-map drift:** root files (README, LICENSE, SESSION-HANDOFF.md, …) predate the map — refresh via `/gsd-map-codebase` when convenient (non-blocking). From 2dc48e6910f460788ef94f043ed759452838137c Mon Sep 17 00:00:00 2001 From: guy-lud Date: Tue, 14 Jul 2026 16:40:38 +0300 Subject: [PATCH 21/21] some cleanups and chanes --- .../CommandLineSettingsBinder.cs | 41 +++++++------------ .../CommandLineSettingsBinderOptions.cs | 2 - .../EnvironmentVariableBinder.cs | 1 - .../EnvironmentVariableBinderOptions.cs | 2 - .../SettingsBuilderFactoryExtensions.cs | 1 - .../ISettingsBuilderOptions.cs | 1 - .../ISettingsProvider.cs | 4 +- .../ServicesSettingsBuilderExtensions.cs | 3 +- .../SettingsBuilderOptions.cs | 4 +- .../SettingsBuilderOptionsExtensions.cs | 3 +- .../SettingsProvider.cs | 2 - .../Binder/InMemoryCollection.cs | 5 +-- .../BindingContext.cs | 1 - .../Conversion/ArrayTypeConverter.cs | 4 +- .../Conversion/CollectionTypeConverter.cs | 2 - .../Conversion/DateTimeTypeConverter.cs | 3 +- .../Conversion/DefaultTypeConverter.cs | 3 +- .../Conversion/EnumTypeConverter.cs | 3 +- .../Conversion/EnumerableTypeConverter.cs | 3 +- .../Conversion/ISettingsTypeConverter.cs | 4 +- .../Conversion/PropertyConversion.cs | 2 - .../Conversion/TypeConvertersCollections.cs | 4 +- .../Conversion/UriTypeConvertor.cs | 4 +- .../Core/ISettingsTypesExtractor.cs | 4 +- .../Reflection/EqualityCompererCreator.cs | 2 - .../Reflection/IEqualityCompererCreator.cs | 3 +- .../Core/Reflection/IPropertyCreator.cs | 1 - .../Reflection/ISettingsClassGenerator.cs | 4 +- .../Reflection/ITypePropertiesExtractor.cs | 2 - .../Core/Reflection/PropertyCreator.cs | 5 +-- .../Core/Reflection/SettingsClassGenerator.cs | 4 +- .../Core/Reflection/TypeConverter.cs | 1 - .../Core/Reflection/TypeExtensions.cs | 4 +- .../Reflection/TypePropertiesExtractor.cs | 5 +-- .../Core/SettingsOptionsValidator.cs | 1 - .../Core/SettingsTypesExtractor.cs | 3 -- .../ISettingsCollection.cs | 5 +-- .../ISettingsHolder.cs | 4 +- .../IValuesPopulator.cs | 3 -- .../ExistForAll.SimpleSettings/Resources.cs | 1 - .../SettingsBindingException.cs | 4 +- .../SettingsBuilder.cs | 3 -- .../SettingsBuilderExtensions.cs | 2 - .../SettingsBuilderFactory.cs | 3 -- .../SettingsBuilderFactoryExtensions.cs | 1 - .../SettingsCollection.cs | 2 - .../SettingsCollectionExtensions.cs | 2 - .../SettingsExtractionException.cs | 2 - .../SettingsHolder.cs | 4 +- .../SettingsOptionNonAttributeException.cs | 2 - .../SettingsOptions.cs | 3 +- ...SettingsOptionsArgumentMissingException.cs | 2 - .../SettingsOptionsArgumentNullException.cs | 2 - .../SettingsPlan.cs | 1 - .../SettingsPropertyAttribute.cs | 4 +- .../SettingsPropertyExtractionException.cs | 2 - .../SettingsPropertyNullException.cs | 2 - .../SettingsPropertyValueException.cs | 1 - .../SettingsSectionAttribute.cs | 4 +- .../SettingsTypeNotFoundException.cs | 4 +- .../SettingsTypeNotInterfaceException.cs | 2 - .../SimpleSettingsException.cs | 2 - .../TypeGenerationException.cs | 2 - .../Validations/ISettingValidation.cs | 2 - .../Validations/ISettingsValidator.cs | 2 - .../Validations/ValidationResult.cs | 4 -- .../ValuesPopulator.cs | 3 -- ...lderConfigurationBinderIntegrationTests.cs | 2 - .../ConfigurationBinderCacheTests.cs | 1 - .../Conversion/CollectionConversionTests.cs | 4 -- .../Conversion/EnumConversionTests.cs | 1 - .../Conversion/ExceptionRedactionTests.cs | 1 - .../Conversion/ScalarConversionTests.cs | 2 - .../Core/TypeConverterTests.cs | 2 - .../AddSimpleSettingsIntegrationTests.cs | 1 - .../DisposableEnvironmentVariable.cs | 5 +-- .../SimpleSettings/BindingContextTests.cs | 3 -- .../EnvironmentVariableAttributeTests.cs | 1 - .../SimpleSettings/ExceptionHierarchyTests.cs | 2 - .../NonDefaultSettingsTypesExtractorTests.cs | 2 - .../SettingsBuilderConversionsTests.cs | 1 - .../SettingsClassGeneratorTests.cs | 4 -- .../SimpleSettings/SettingsCollectionTests.cs | 2 - .../SimpleSettings/SettingsEnumerableTests.cs | 3 -- .../SettingsOptionsValidatorTests.cs | 1 - .../SettingsTypesExtractorTests.cs | 2 - .../ConfigBinderBenchmark.cs | 1 - .../IPerformanceInterfaces.cs | 2 - .../MicroBenchmarks.cs | 2 - .../PlanPopulateBenchmark.cs | 1 - .../ResolveBenchmark.cs | 1 - .../ScanBenchmark.cs | 1 - .../ShapeBenchmark.cs | 1 - 93 files changed, 43 insertions(+), 227 deletions(-) diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs index bc001a9..c0b8748 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinder.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using ExistForAll.SimpleSettings; -using ExistForAll.SimpleSettings.Binders; - namespace ExistForAll.SimpleSettings.Binders { public class CommandLineSettingsBinder : ISectionBinder @@ -26,6 +20,7 @@ public CommandLineSettingsBinder(string[] args, CommandLineSettingsBinderOptions public void BindPropertySettings(BindingContext context) { + var key = _options.NameFormatter != null ? _options.NameFormatter(context.Section, context.Key) : context.Key; if(_argumentStore.TryGetValue(key, out var value)) @@ -34,7 +29,7 @@ public void BindPropertySettings(BindingContext context) } } - private void Parse(string[] args) + private void Parse(string[]? args) { _argumentStore.Clear(); @@ -61,7 +56,6 @@ private void Parse(string[] args) if (!tokenWasPrefixed || index + 1 >= args.Length) continue; var next = args[index + 1]; - if (next is null) continue; if (next.Length > 0 && Array.IndexOf(prefixes, next[0]) >= 0) continue; _argumentStore[name] = next; @@ -69,7 +63,7 @@ private void Parse(string[] args) } } - private static Tuple? SplitByDelimiter(string str, CommandLineSettingsBinderOptions options) + private static Tuple? SplitByDelimiter(string? str, CommandLineSettingsBinderOptions options) { if (str == null) return null; @@ -77,31 +71,24 @@ private void Parse(string[] args) string key; string? value; - if (!options.Delimiters.Any()) + if (!options.Delimiters.Any()) + return new Tuple(str.Trim(), null); + + var indices = options.Delimiters + .Select(d => str.IndexOf(d, StringComparison.Ordinal)) + .Where(d => d != -1).ToList(); + + if (indices.Count == 0) { key = str.Trim(); value = null; } else { - - var indices = options.Delimiters.Where(delimiter => delimiter != null) - .Select(d => str.IndexOf(d)) - .Where(d => d != -1).ToList(); - - if (indices.Count == 0) - { - key = str.Trim(); - value = null; - } - else - { - var idx = indices.OrderBy(i => i).First(); - key = str.Substring(0, idx); - value = str.Substring(idx + 1); - } + var idx = indices.OrderBy(i => i).First(); + key = str[..idx]; + value = str[(idx + 1)..]; } - return new Tuple(key, value); } } diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs index 5a51ded..e6702c6 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/CommandLineSettingsBinderOptions.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using ExistForAll.SimpleSettings.Binder; namespace ExistForAll.SimpleSettings.Binders diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/EnvironmentVariableBinder.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/EnvironmentVariableBinder.cs index 9630732..6df59f9 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/EnvironmentVariableBinder.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/EnvironmentVariableBinder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; namespace ExistForAll.SimpleSettings.Binders diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/EnvironmentVariableBinderOptions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/EnvironmentVariableBinderOptions.cs index 91a78bf..262cc58 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/EnvironmentVariableBinderOptions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/EnvironmentVariableBinderOptions.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings.Binders { public class EnvironmentVariableBinderOptions diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs index 073c136..ba14e84 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.Binders/SettingsBuilderFactoryExtensions.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.Configuration; namespace ExistForAll.SimpleSettings.Binders diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ISettingsBuilderOptions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ISettingsBuilderOptions.cs index edfdcb8..00f36e5 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ISettingsBuilderOptions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ISettingsBuilderOptions.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Reflection; namespace ExistForAll.SimpleSettings.Extensions.GenericHost diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ISettingsProvider.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ISettingsProvider.cs index 5999487..4b2eb44 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ISettingsProvider.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ISettingsProvider.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings.Extensions.GenericHost +namespace ExistForAll.SimpleSettings.Extensions.GenericHost { public interface ISettingsProvider { diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ServicesSettingsBuilderExtensions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ServicesSettingsBuilderExtensions.cs index d740734..d938ebe 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ServicesSettingsBuilderExtensions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/ServicesSettingsBuilderExtensions.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; namespace ExistForAll.SimpleSettings.Extensions.GenericHost { diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsBuilderOptions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsBuilderOptions.cs index f01b6df..8527457 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsBuilderOptions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsBuilderOptions.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Reflection; +using System.Reflection; namespace ExistForAll.SimpleSettings.Extensions.GenericHost { diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsBuilderOptionsExtensions.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsBuilderOptionsExtensions.cs index 132db64..a802f25 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsBuilderOptionsExtensions.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsBuilderOptionsExtensions.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Reflection; +using System.Reflection; namespace ExistForAll.SimpleSettings.Extensions.GenericHost { diff --git a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsProvider.cs b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsProvider.cs index 2ae8f66..86f9811 100644 --- a/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsProvider.cs +++ b/src/Core/ExistForAll.SimpleSettings.Extensions.GenericHost/SettingsProvider.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings.Extensions.GenericHost { internal class SettingsProvider : ISettingsProvider diff --git a/src/Core/ExistForAll.SimpleSettings/Binder/InMemoryCollection.cs b/src/Core/ExistForAll.SimpleSettings/Binder/InMemoryCollection.cs index 8488ed6..8047d1e 100644 --- a/src/Core/ExistForAll.SimpleSettings/Binder/InMemoryCollection.cs +++ b/src/Core/ExistForAll.SimpleSettings/Binder/InMemoryCollection.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace ExistForAll.SimpleSettings.Binder +namespace ExistForAll.SimpleSettings.Binder { public class InMemoryCollection : IInMemoryCollection { diff --git a/src/Core/ExistForAll.SimpleSettings/BindingContext.cs b/src/Core/ExistForAll.SimpleSettings/BindingContext.cs index 1b960e0..e2936e3 100644 --- a/src/Core/ExistForAll.SimpleSettings/BindingContext.cs +++ b/src/Core/ExistForAll.SimpleSettings/BindingContext.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; namespace ExistForAll.SimpleSettings diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/ArrayTypeConverter.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/ArrayTypeConverter.cs index 112dbe1..54e558b 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/ArrayTypeConverter.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/ArrayTypeConverter.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings.Conversion +namespace ExistForAll.SimpleSettings.Conversion { internal class ArrayTypeConverter : CollectionTypeConverter { diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/CollectionTypeConverter.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/CollectionTypeConverter.cs index 32496b9..302a068 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/CollectionTypeConverter.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/CollectionTypeConverter.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings.Conversion { // Shared conversion for the two collection shapes — arrays and IEnumerable properties. Both split a diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/DateTimeTypeConverter.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/DateTimeTypeConverter.cs index d3dded6..75b4c56 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/DateTimeTypeConverter.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/DateTimeTypeConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Globalization; +using System.Globalization; namespace ExistForAll.SimpleSettings.Conversion { diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/DefaultTypeConverter.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/DefaultTypeConverter.cs index 6beab48..418355e 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/DefaultTypeConverter.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/DefaultTypeConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Globalization; +using System.Globalization; namespace ExistForAll.SimpleSettings.Conversion { diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/EnumTypeConverter.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/EnumTypeConverter.cs index 3261bfe..bb6d575 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/EnumTypeConverter.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/EnumTypeConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Reflection; +using System.Reflection; namespace ExistForAll.SimpleSettings.Conversion { diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/EnumerableTypeConverter.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/EnumerableTypeConverter.cs index bb3e4b4..e3b7eaf 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/EnumerableTypeConverter.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/EnumerableTypeConverter.cs @@ -1,5 +1,4 @@ -using System; -using System.Reflection; +using System.Reflection; using ExistForAll.SimpleSettings.Core.Reflection; namespace ExistForAll.SimpleSettings.Conversion diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/ISettingsTypeConverter.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/ISettingsTypeConverter.cs index 69c46f0..1646074 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/ISettingsTypeConverter.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/ISettingsTypeConverter.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings.Conversion +namespace ExistForAll.SimpleSettings.Conversion { /// /// Converts a bound settings value to a property's target type. diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/PropertyConversion.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/PropertyConversion.cs index 8e3e8d9..30232e6 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/PropertyConversion.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/PropertyConversion.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings.Conversion { // A per-property conversion resolved once (at plan build) instead of on every populate: the chosen diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/TypeConvertersCollections.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/TypeConvertersCollections.cs index 67986a1..d64747f 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/TypeConvertersCollections.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/TypeConvertersCollections.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace ExistForAll.SimpleSettings.Conversion +namespace ExistForAll.SimpleSettings.Conversion { internal class TypeConvertersCollections : LinkedList { diff --git a/src/Core/ExistForAll.SimpleSettings/Conversion/UriTypeConvertor.cs b/src/Core/ExistForAll.SimpleSettings/Conversion/UriTypeConvertor.cs index 2167320..6347e37 100644 --- a/src/Core/ExistForAll.SimpleSettings/Conversion/UriTypeConvertor.cs +++ b/src/Core/ExistForAll.SimpleSettings/Conversion/UriTypeConvertor.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings.Conversion +namespace ExistForAll.SimpleSettings.Conversion { internal class UriTypeConvertor : ISettingsTypeConverter { diff --git a/src/Core/ExistForAll.SimpleSettings/Core/ISettingsTypesExtractor.cs b/src/Core/ExistForAll.SimpleSettings/Core/ISettingsTypesExtractor.cs index f79da49..a7c876c 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/ISettingsTypesExtractor.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/ISettingsTypesExtractor.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Reflection; +using System.Reflection; namespace ExistForAll.SimpleSettings.Core { diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/EqualityCompererCreator.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/EqualityCompererCreator.cs index c6e7957..05ad040 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/EqualityCompererCreator.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/EqualityCompererCreator.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/IEqualityCompererCreator.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/IEqualityCompererCreator.cs index af7598e..c9ca8cd 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/IEqualityCompererCreator.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/IEqualityCompererCreator.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Reflection; +using System.Reflection; using System.Reflection.Emit; namespace ExistForAll.SimpleSettings.Core.Reflection diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/IPropertyCreator.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/IPropertyCreator.cs index 17f00c2..4ab5a2f 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/IPropertyCreator.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/IPropertyCreator.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/ISettingsClassGenerator.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/ISettingsClassGenerator.cs index 81b0d36..cd9e7a5 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/ISettingsClassGenerator.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/ISettingsClassGenerator.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings.Core.Reflection +namespace ExistForAll.SimpleSettings.Core.Reflection { internal interface ISettingsClassGenerator { diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/ITypePropertiesExtractor.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/ITypePropertiesExtractor.cs index 1b97243..4472aa5 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/ITypePropertiesExtractor.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/ITypePropertiesExtractor.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Reflection; namespace ExistForAll.SimpleSettings.Core.Reflection diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/PropertyCreator.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/PropertyCreator.cs index e933fb5..8e1e0b7 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/PropertyCreator.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/PropertyCreator.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; +using System.Reflection; using System.Reflection.Emit; namespace ExistForAll.SimpleSettings.Core.Reflection diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/SettingsClassGenerator.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/SettingsClassGenerator.cs index f9fd29a..c210f36 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/SettingsClassGenerator.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/SettingsClassGenerator.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Reflection; using System.Reflection.Emit; diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypeConverter.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypeConverter.cs index 464661e..663338f 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypeConverter.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypeConverter.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; using ExistForAll.SimpleSettings.Conversion; diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypeExtensions.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypeExtensions.cs index d0a0b9a..1d6db31 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypeExtensions.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypeExtensions.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Reflection; +using System.Reflection; namespace ExistForAll.SimpleSettings.Core.Reflection { diff --git a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypePropertiesExtractor.cs b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypePropertiesExtractor.cs index ebde463..df2ab6c 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypePropertiesExtractor.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/Reflection/TypePropertiesExtractor.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Reflection; namespace ExistForAll.SimpleSettings.Core.Reflection diff --git a/src/Core/ExistForAll.SimpleSettings/Core/SettingsOptionsValidator.cs b/src/Core/ExistForAll.SimpleSettings/Core/SettingsOptionsValidator.cs index 8da767c..bd7d863 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/SettingsOptionsValidator.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/SettingsOptionsValidator.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; namespace ExistForAll.SimpleSettings.Core diff --git a/src/Core/ExistForAll.SimpleSettings/Core/SettingsTypesExtractor.cs b/src/Core/ExistForAll.SimpleSettings/Core/SettingsTypesExtractor.cs index 58102bc..2fbe6af 100644 --- a/src/Core/ExistForAll.SimpleSettings/Core/SettingsTypesExtractor.cs +++ b/src/Core/ExistForAll.SimpleSettings/Core/SettingsTypesExtractor.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; namespace ExistForAll.SimpleSettings.Core diff --git a/src/Core/ExistForAll.SimpleSettings/ISettingsCollection.cs b/src/Core/ExistForAll.SimpleSettings/ISettingsCollection.cs index bd2c374..d9416bb 100644 --- a/src/Core/ExistForAll.SimpleSettings/ISettingsCollection.cs +++ b/src/Core/ExistForAll.SimpleSettings/ISettingsCollection.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace ExistForAll.SimpleSettings +namespace ExistForAll.SimpleSettings { public interface ISettingsCollection : IEnumerable> { diff --git a/src/Core/ExistForAll.SimpleSettings/ISettingsHolder.cs b/src/Core/ExistForAll.SimpleSettings/ISettingsHolder.cs index 49d8b21..9c0d9ff 100644 --- a/src/Core/ExistForAll.SimpleSettings/ISettingsHolder.cs +++ b/src/Core/ExistForAll.SimpleSettings/ISettingsHolder.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings +namespace ExistForAll.SimpleSettings { internal interface ISettingsHolder { diff --git a/src/Core/ExistForAll.SimpleSettings/IValuesPopulator.cs b/src/Core/ExistForAll.SimpleSettings/IValuesPopulator.cs index 13b426e..217b665 100644 --- a/src/Core/ExistForAll.SimpleSettings/IValuesPopulator.cs +++ b/src/Core/ExistForAll.SimpleSettings/IValuesPopulator.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace ExistForAll.SimpleSettings { internal interface IValuesPopulator diff --git a/src/Core/ExistForAll.SimpleSettings/Resources.cs b/src/Core/ExistForAll.SimpleSettings/Resources.cs index 7b3c101..69f9dd6 100644 --- a/src/Core/ExistForAll.SimpleSettings/Resources.cs +++ b/src/Core/ExistForAll.SimpleSettings/Resources.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; namespace ExistForAll.SimpleSettings diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsBindingException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsBindingException.cs index 0d9b223..cf43640 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsBindingException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsBindingException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings +namespace ExistForAll.SimpleSettings { // Raised when an ISectionBinder throws while binding a property. It carries the binder type, section, and // key as primitives and deliberately does NOT retain the BindingContext — the context holds the bound value diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsBuilder.cs b/src/Core/ExistForAll.SimpleSettings/SettingsBuilder.cs index e5dbfdd..e654203 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsBuilder.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsBuilder.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using ExistForAll.SimpleSettings.Core; using ExistForAll.SimpleSettings.Core.Reflection; diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsBuilderExtensions.cs b/src/Core/ExistForAll.SimpleSettings/SettingsBuilderExtensions.cs index 5d30992..e5a5784 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsBuilderExtensions.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsBuilderExtensions.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using System.Reflection; namespace ExistForAll.SimpleSettings diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsBuilderFactory.cs b/src/Core/ExistForAll.SimpleSettings/SettingsBuilderFactory.cs index 4f6c85e..7bb3443 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsBuilderFactory.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsBuilderFactory.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace ExistForAll.SimpleSettings { internal class SettingsBuilderFactory : ISettingsBuilderFactory diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsBuilderFactoryExtensions.cs b/src/Core/ExistForAll.SimpleSettings/SettingsBuilderFactoryExtensions.cs index 3ac53d3..6ac01c8 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsBuilderFactoryExtensions.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsBuilderFactoryExtensions.cs @@ -1,4 +1,3 @@ -using System; using ExistForAll.SimpleSettings.Binder; using ExistForAll.SimpleSettings.Conversion; diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsCollection.cs b/src/Core/ExistForAll.SimpleSettings/SettingsCollection.cs index 276225f..047ef3d 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsCollection.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsCollection.cs @@ -1,6 +1,4 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Reflection; namespace ExistForAll.SimpleSettings diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsCollectionExtensions.cs b/src/Core/ExistForAll.SimpleSettings/SettingsCollectionExtensions.cs index 6b1d899..af22e97 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsCollectionExtensions.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsCollectionExtensions.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { public static class SettingsCollectionExtensions diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsExtractionException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsExtractionException.cs index ee83fae..dad267c 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsExtractionException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsExtractionException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { // Moved to the root namespace (was ExistForAll.SimpleSettings.Core) so the public exception surface is diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs b/src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs index 704c02a..822a929 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsHolder.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings +namespace ExistForAll.SimpleSettings { internal sealed class SettingsHolder : ISettingsHolder { diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsOptionNonAttributeException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsOptionNonAttributeException.cs index 5cf54dc..52c0b62 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsOptionNonAttributeException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsOptionNonAttributeException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { public class SettingsOptionNonAttributeException : SimpleSettingsException diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsOptions.cs b/src/Core/ExistForAll.SimpleSettings/SettingsOptions.cs index 31ccc74..285d740 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsOptions.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsOptions.cs @@ -1,5 +1,4 @@ -using System; -using ExistForAll.SimpleSettings.Conversion; +using ExistForAll.SimpleSettings.Conversion; using ExistForAll.SimpleSettings.Core.Reflection; namespace ExistForAll.SimpleSettings diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsOptionsArgumentMissingException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsOptionsArgumentMissingException.cs index 38ccd7f..3c4184a 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsOptionsArgumentMissingException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsOptionsArgumentMissingException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { public class SettingsOptionsArgumentMissingException : SimpleSettingsException diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsOptionsArgumentNullException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsOptionsArgumentNullException.cs index e6175df..4493b57 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsOptionsArgumentNullException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsOptionsArgumentNullException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { public class SettingsOptionsArgumentNullException : SimpleSettingsException diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsPlan.cs b/src/Core/ExistForAll.SimpleSettings/SettingsPlan.cs index fd1fd54..0dad595 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsPlan.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsPlan.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; using ExistForAll.SimpleSettings.Conversion; using ExistForAll.SimpleSettings.Core.Reflection; diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsPropertyAttribute.cs b/src/Core/ExistForAll.SimpleSettings/SettingsPropertyAttribute.cs index 36e2958..4c96d18 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsPropertyAttribute.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsPropertyAttribute.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings +namespace ExistForAll.SimpleSettings { [AttributeUsage(AttributeTargets.Property)] public class SettingsPropertyAttribute : Attribute diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsPropertyExtractionException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsPropertyExtractionException.cs index a20bb78..7c9f63f 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsPropertyExtractionException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsPropertyExtractionException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { // Promoted to public and moved to the root namespace (was internal in ExistForAll.SimpleSettings.Core.Reflection): diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsPropertyNullException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsPropertyNullException.cs index 8d5ec97..769bf40 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsPropertyNullException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsPropertyNullException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { // Raised when a property marked [SettingsProperty(AllowEmpty = false)] resolves to no value. Distinct from diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsPropertyValueException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsPropertyValueException.cs index 7a469af..a5a9b61 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsPropertyValueException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsPropertyValueException.cs @@ -1,4 +1,3 @@ -using System; using System.Reflection; namespace ExistForAll.SimpleSettings diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsSectionAttribute.cs b/src/Core/ExistForAll.SimpleSettings/SettingsSectionAttribute.cs index 51ea9a0..50de319 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsSectionAttribute.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsSectionAttribute.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings +namespace ExistForAll.SimpleSettings { [AttributeUsage(AttributeTargets.Interface)] public class SettingsSectionAttribute : Attribute diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsTypeNotFoundException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsTypeNotFoundException.cs index 3e8a715..6f54049 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsTypeNotFoundException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsTypeNotFoundException.cs @@ -1,6 +1,4 @@ -using System; - -namespace ExistForAll.SimpleSettings +namespace ExistForAll.SimpleSettings { public class SettingsTypeNotFoundException : SimpleSettingsException { diff --git a/src/Core/ExistForAll.SimpleSettings/SettingsTypeNotInterfaceException.cs b/src/Core/ExistForAll.SimpleSettings/SettingsTypeNotInterfaceException.cs index 1d728ce..904d580 100644 --- a/src/Core/ExistForAll.SimpleSettings/SettingsTypeNotInterfaceException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SettingsTypeNotInterfaceException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { // Raised when a type handed to the builder/collection is not an interface (SimpleSettings generates its diff --git a/src/Core/ExistForAll.SimpleSettings/SimpleSettingsException.cs b/src/Core/ExistForAll.SimpleSettings/SimpleSettingsException.cs index 6deea51..be08fa8 100644 --- a/src/Core/ExistForAll.SimpleSettings/SimpleSettingsException.cs +++ b/src/Core/ExistForAll.SimpleSettings/SimpleSettingsException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { // Common base for every exception the library throws, so consumers can catch the whole family with a single diff --git a/src/Core/ExistForAll.SimpleSettings/TypeGenerationException.cs b/src/Core/ExistForAll.SimpleSettings/TypeGenerationException.cs index c099ac6..e1d848a 100644 --- a/src/Core/ExistForAll.SimpleSettings/TypeGenerationException.cs +++ b/src/Core/ExistForAll.SimpleSettings/TypeGenerationException.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings { // Promoted to public and moved to the root namespace (was internal in ExistForAll.SimpleSettings.Core.Reflection): diff --git a/src/Core/ExistForAll.SimpleSettings/Validations/ISettingValidation.cs b/src/Core/ExistForAll.SimpleSettings/Validations/ISettingValidation.cs index 49f7806..293f9e1 100644 --- a/src/Core/ExistForAll.SimpleSettings/Validations/ISettingValidation.cs +++ b/src/Core/ExistForAll.SimpleSettings/Validations/ISettingValidation.cs @@ -1,5 +1,3 @@ -using System.Threading.Tasks; - namespace ExistForAll.SimpleSettings.Validations { public interface ISettingValidation : ISettingsValidator diff --git a/src/Core/ExistForAll.SimpleSettings/Validations/ISettingsValidator.cs b/src/Core/ExistForAll.SimpleSettings/Validations/ISettingsValidator.cs index 848470e..c4f6953 100644 --- a/src/Core/ExistForAll.SimpleSettings/Validations/ISettingsValidator.cs +++ b/src/Core/ExistForAll.SimpleSettings/Validations/ISettingsValidator.cs @@ -1,5 +1,3 @@ -using System.Threading.Tasks; - namespace ExistForAll.SimpleSettings.Validations { public interface ISettingsValidator diff --git a/src/Core/ExistForAll.SimpleSettings/Validations/ValidationResult.cs b/src/Core/ExistForAll.SimpleSettings/Validations/ValidationResult.cs index 3365b0a..700eb51 100644 --- a/src/Core/ExistForAll.SimpleSettings/Validations/ValidationResult.cs +++ b/src/Core/ExistForAll.SimpleSettings/Validations/ValidationResult.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; - namespace ExistForAll.SimpleSettings.Validations { public class ValidationResult diff --git a/src/Core/ExistForAll.SimpleSettings/ValuesPopulator.cs b/src/Core/ExistForAll.SimpleSettings/ValuesPopulator.cs index 4408de7..9d366bd 100644 --- a/src/Core/ExistForAll.SimpleSettings/ValuesPopulator.cs +++ b/src/Core/ExistForAll.SimpleSettings/ValuesPopulator.cs @@ -1,7 +1,4 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using ExistForAll.SimpleSettings.Core.Reflection; diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/ConfigBuilderConfigurationBinderIntegrationTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/ConfigBuilderConfigurationBinderIntegrationTests.cs index 12aa539..94bb1ff 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/ConfigBuilderConfigurationBinderIntegrationTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/ConfigBuilderConfigurationBinderIntegrationTests.cs @@ -1,5 +1,3 @@ -using System; -using System.IO; using ExistForAll.SimpleSettings.Binder; using ExistForAll.SimpleSettings.Binders; using Microsoft.Extensions.Configuration; diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/ConfigurationBinderCacheTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/ConfigurationBinderCacheTests.cs index 5542e8a..3ec3a14 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/ConfigurationBinderCacheTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/ConfigurationBinderCacheTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using ExistForAll.SimpleSettings.Binders; using Microsoft.Extensions.Configuration; diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/CollectionConversionTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/CollectionConversionTests.cs index 887b0c2..163bfe4 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/CollectionConversionTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/CollectionConversionTests.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using ExistForAll.SimpleSettings.Binder; -using ExistForAll.SimpleSettings.Binders; namespace ExistForAll.SimpleSettings.UnitTests.Conversion { diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/EnumConversionTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/EnumConversionTests.cs index b41de55..a90613b 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/EnumConversionTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/EnumConversionTests.cs @@ -1,4 +1,3 @@ -using System; using ExistForAll.SimpleSettings.Binder; namespace ExistForAll.SimpleSettings.UnitTests.Conversion diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/ExceptionRedactionTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/ExceptionRedactionTests.cs index 8014192..3d1a1b3 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/ExceptionRedactionTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/ExceptionRedactionTests.cs @@ -1,4 +1,3 @@ -using System; using ExistForAll.SimpleSettings.Binder; using ExistForAll.SimpleSettings.Conversion; diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/ScalarConversionTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/ScalarConversionTests.cs index da014cb..a6580d7 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/ScalarConversionTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Conversion/ScalarConversionTests.cs @@ -1,6 +1,4 @@ -using System; using ExistForAll.SimpleSettings.Binder; -using ExistForAll.SimpleSettings.Binders; namespace ExistForAll.SimpleSettings.UnitTests.Conversion { diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Core/TypeConverterTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Core/TypeConverterTests.cs index 1406921..093cfe2 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/Core/TypeConverterTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/Core/TypeConverterTests.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using System.Reflection; using ExistForAll.SimpleSettings.Conversion; using ExistForAll.SimpleSettings.Core.Reflection; diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/DependencyInjection/AddSimpleSettingsIntegrationTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/DependencyInjection/AddSimpleSettingsIntegrationTests.cs index 470274f..0847d80 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/DependencyInjection/AddSimpleSettingsIntegrationTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/DependencyInjection/AddSimpleSettingsIntegrationTests.cs @@ -1,4 +1,3 @@ -using System; using ExistForAll.SimpleSettings.Binder; using ExistForAll.SimpleSettings.Extensions.GenericHost; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/DisposableEnvironmentVariable.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/DisposableEnvironmentVariable.cs index d14401b..7ae080b 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/DisposableEnvironmentVariable.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/DisposableEnvironmentVariable.cs @@ -1,7 +1,4 @@ -using System; -using System.Threading; - -namespace ExistForAll.SimpleSettings.UnitTests +namespace ExistForAll.SimpleSettings.UnitTests { internal class DisposableEnvironmentVariable : IDisposable { diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/BindingContextTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/BindingContextTests.cs index 9a3d044..fd76780 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/BindingContextTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/BindingContextTests.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; - namespace ExistForAll.SimpleSettings.UnitTests.SimpleSettings { public class BindingContextTests diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/EnvironmentVariableAttributeTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/EnvironmentVariableAttributeTests.cs index 3ac4ba4..ef7ed8b 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/EnvironmentVariableAttributeTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/EnvironmentVariableAttributeTests.cs @@ -1,4 +1,3 @@ -using System; using ExistForAll.SimpleSettings.Binder; using ExistForAll.SimpleSettings.Binders; diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/ExceptionHierarchyTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/ExceptionHierarchyTests.cs index b7f35b4..058d6d8 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/ExceptionHierarchyTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/ExceptionHierarchyTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using ExistForAll.SimpleSettings.Binder; namespace ExistForAll.SimpleSettings.UnitTests.SimpleSettings diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/NonDefaultSettingsTypesExtractorTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/NonDefaultSettingsTypesExtractorTests.cs index 97460ac..bb0808f 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/NonDefaultSettingsTypesExtractorTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/NonDefaultSettingsTypesExtractorTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Reflection; using ExistForAll.SimpleSettings.Core; diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsBuilderConversionsTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsBuilderConversionsTests.cs index 36edc7f..b2541f7 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsBuilderConversionsTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsBuilderConversionsTests.cs @@ -1,4 +1,3 @@ -using System; using ExistForAll.SimpleSettings.Conversion; namespace ExistForAll.SimpleSettings.UnitTests.SimpleSettings diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsClassGeneratorTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsClassGeneratorTests.cs index 35524ff..85ff978 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsClassGeneratorTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsClassGeneratorTests.cs @@ -1,9 +1,5 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Threading; using ExistForAll.SimpleSettings.Core.Reflection; namespace ExistForAll.SimpleSettings.UnitTests.SimpleSettings diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsCollectionTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsCollectionTests.cs index abb00fb..7b56197 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsCollectionTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsCollectionTests.cs @@ -1,5 +1,3 @@ -using System; - namespace ExistForAll.SimpleSettings.UnitTests.SimpleSettings { public class SettingsCollectionTests diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsEnumerableTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsEnumerableTests.cs index a27594b..9041d90 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsEnumerableTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsEnumerableTests.cs @@ -1,6 +1,3 @@ -using System.Collections.Generic; -using System.Linq; - namespace ExistForAll.SimpleSettings.UnitTests.SimpleSettings { public class SettingsEnumerableTests diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsOptionsValidatorTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsOptionsValidatorTests.cs index 150db5a..175cfcf 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsOptionsValidatorTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsOptionsValidatorTests.cs @@ -1,4 +1,3 @@ -using System; using ExistForAll.SimpleSettings.Core; namespace ExistForAll.SimpleSettings.UnitTests.SimpleSettings diff --git a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsTypesExtractorTests.cs b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsTypesExtractorTests.cs index 86a3994..1511b5e 100644 --- a/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsTypesExtractorTests.cs +++ b/src/Tests/ExistForAll.SimpleSettings.UnitTests/SimpleSettings/SettingsTypesExtractorTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Reflection; using ExistForAll.SimpleSettings.Core; diff --git a/src/performance/ExistForAll.SimpleSettings.Benchmark/ConfigBinderBenchmark.cs b/src/performance/ExistForAll.SimpleSettings.Benchmark/ConfigBinderBenchmark.cs index 46e5587..704fa59 100644 --- a/src/performance/ExistForAll.SimpleSettings.Benchmark/ConfigBinderBenchmark.cs +++ b/src/performance/ExistForAll.SimpleSettings.Benchmark/ConfigBinderBenchmark.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using BenchmarkDotNet.Attributes; using ExistForAll.SimpleSettings.Binders; using Microsoft.Extensions.Configuration; diff --git a/src/performance/ExistForAll.SimpleSettings.Benchmark/IPerformanceInterfaces.cs b/src/performance/ExistForAll.SimpleSettings.Benchmark/IPerformanceInterfaces.cs index 6fa76a5..cbc7250 100644 --- a/src/performance/ExistForAll.SimpleSettings.Benchmark/IPerformanceInterfaces.cs +++ b/src/performance/ExistForAll.SimpleSettings.Benchmark/IPerformanceInterfaces.cs @@ -1,5 +1,3 @@ -using ExistForAll.SimpleSettings; - namespace ExistForAll.SimpleSettings.Benchmark { [SettingsSection] public interface IPerformanceInterface0 {[SettingsProperty(DefaultValue = "name_0")] string Name { get; set; } } diff --git a/src/performance/ExistForAll.SimpleSettings.Benchmark/MicroBenchmarks.cs b/src/performance/ExistForAll.SimpleSettings.Benchmark/MicroBenchmarks.cs index 5f0d8a1..81bc79a 100644 --- a/src/performance/ExistForAll.SimpleSettings.Benchmark/MicroBenchmarks.cs +++ b/src/performance/ExistForAll.SimpleSettings.Benchmark/MicroBenchmarks.cs @@ -1,5 +1,3 @@ -using System; -using System.Reflection; using BenchmarkDotNet.Attributes; using ExistForAll.SimpleSettings.Binders; using ExistForAll.SimpleSettings.Conversion; diff --git a/src/performance/ExistForAll.SimpleSettings.Benchmark/PlanPopulateBenchmark.cs b/src/performance/ExistForAll.SimpleSettings.Benchmark/PlanPopulateBenchmark.cs index f6f48ed..d0f3d0b 100644 --- a/src/performance/ExistForAll.SimpleSettings.Benchmark/PlanPopulateBenchmark.cs +++ b/src/performance/ExistForAll.SimpleSettings.Benchmark/PlanPopulateBenchmark.cs @@ -1,4 +1,3 @@ -using System; using BenchmarkDotNet.Attributes; using ExistForAll.SimpleSettings.Binder; diff --git a/src/performance/ExistForAll.SimpleSettings.Benchmark/ResolveBenchmark.cs b/src/performance/ExistForAll.SimpleSettings.Benchmark/ResolveBenchmark.cs index bcd684f..156e0a8 100644 --- a/src/performance/ExistForAll.SimpleSettings.Benchmark/ResolveBenchmark.cs +++ b/src/performance/ExistForAll.SimpleSettings.Benchmark/ResolveBenchmark.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Attributes; -using ExistForAll.SimpleSettings; using ExistForAll.SimpleSettings.Binder; using Microsoft.Extensions.DependencyInjection; diff --git a/src/performance/ExistForAll.SimpleSettings.Benchmark/ScanBenchmark.cs b/src/performance/ExistForAll.SimpleSettings.Benchmark/ScanBenchmark.cs index 1303b21..29c7170 100644 --- a/src/performance/ExistForAll.SimpleSettings.Benchmark/ScanBenchmark.cs +++ b/src/performance/ExistForAll.SimpleSettings.Benchmark/ScanBenchmark.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Attributes; -using ExistForAll.SimpleSettings; namespace ExistForAll.SimpleSettings.Benchmark { diff --git a/src/performance/ExistForAll.SimpleSettings.Benchmark/ShapeBenchmark.cs b/src/performance/ExistForAll.SimpleSettings.Benchmark/ShapeBenchmark.cs index 00246c9..781f3f9 100644 --- a/src/performance/ExistForAll.SimpleSettings.Benchmark/ShapeBenchmark.cs +++ b/src/performance/ExistForAll.SimpleSettings.Benchmark/ShapeBenchmark.cs @@ -1,5 +1,4 @@ using BenchmarkDotNet.Attributes; -using ExistForAll.SimpleSettings; using ExistForAll.SimpleSettings.Binder; namespace ExistForAll.SimpleSettings.Benchmark