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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions FIX-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ _Derived from the 2026-07-10 three-part review (architecture · tests · perform
- **P4 merged (#25)** — de-reflect + DRY the array/enumerable converters: shared `CollectionTypeConverter` (`Array.CreateInstance` + indexed fill, manual `LinkedList` walk); `CreateNullResult` de-reflected. Gated `ConvertArrayBenchmark`: **1.33 KB→688 B (−49%), 5.7×**. Suite 68/TFM. Included a user modernization pass (collection expressions across ~19 files).
- **P5 merged (#26)** — resolve config section once per type: `ConfigurationBinder` caches the `IConfigurationSection` per section name (`ConcurrentDictionary`, zero-capture `GetOrAdd`). Plan reviewed by architect+perf+security (chose internal cache over a contract change — layering); code reviewed via `/code-review`. Gated `ConfigBinderBenchmark`: **BindNoRoot 80→40 B (−50%), BindWithRoot 144→56 B (−61%)**. Suite **71/TFM**. Also gated P4's `ConvertArrayBenchmark` (was never in the CI filter). **Security review surfaced a pre-existing leak → new item S1.**
- **Perf track P0–P5 = COMPLETE + merged.** `master` @ `498fc81`.
- **S1 shipped** (branch `security/s1-redact-exception-value`, PR open) — **full fix (Rec 1)**: conversion-failure exceptions no longer carry the bound value **or** chain the value-bearing framework inner (only the failure's type name is surfaced); the value-free "required value missing" case split into its own `SettingsPropertyNullException` so its useful message survives. Three-specialist plan review (security ENDORSE-WITH-CHANGES; architect/perf in-context — kit agents misfired) + `/code-review` (clean, 0 antipatterns). Suite **76 net10** (was 71; +5 redaction tests). Detail in §S1 below.
- **In flight:** S1 PR open (branch `security/s1-redact-exception-value`) — carries this fix-plan + handoff refresh.
- **Next:** engine tests (T4 `ValuesPopulator` / T5 `TypeConverter` / T7 generator concurrency) **or** batch the breaking cleanups (C2 public exception base / A5 / C1 / A6 / A3 / A4) while still pre-stable · optional P3b (tiered/lazy compiled setter, only if set *time* shows in a profile).
- **S1 shipped + merged (#27).** Conversion-failure exceptions no longer carry the bound value **or** chain the value-bearing framework inner; the value-free "required value missing" case split into its own `SettingsPropertyNullException`. `master` @ `5277c60`. Detail in §S1 below.
- **C2 shipped** (branch `refactor/c2-exception-hierarchy`, PR open) — **public exception hierarchy**: `SimpleSettingsException` base, reparent all 10, promote the 4 escapees to public, flatten 3 to root namespace, leak-safe structured properties, retype the `TypeIsNotInterface` throw. Plan reviewed by `security-auditor` + `dotnet-architect` (both ENDORSE-WITH-CHANGES, both fired cleanly) + perf in-context; code by `/code-review`. Suite **82 net10** (was 76; +6). Detail in §C2 below.
- **In flight:** C2 PR open (branch `refactor/c2-exception-hierarchy`) — carries this fix-plan + handoff refresh.
- **Next:** engine tests (T4 `ValuesPopulator` / T5 `TypeConverter` / T7 generator concurrency race) **or** continue the pre-stable breaking cleanups (A5 make `SettingsHolder` internal / C1 `List<T>` support / A6 command-line quoting / A3 `Core.AspNet` / A4 dependency floor) · **A1 (HIGH)** AOT/trim story · optional P3b.
- **C3 — DECIDED (option 2):** cache in the provider only; Core `SettingsBuilder.GetSettings` unchanged; no reload. See #17.
- **Held — do NOT delete (feature work coming):** D1 Validations (reconcile with the `validate-settings` branch) · D2 EqualityCompererCreator.
- Running status lives in `SESSION-HANDOFF.md`.
Expand Down Expand Up @@ -44,7 +45,7 @@ _Derived from the 2026-07-10 three-part review (architecture · tests · perform

**Phase 3 — Correctness/feature (needs design)**
- [ ] C1 · Decide: support `List<T>`/`IList<T>`/`ICollection<T>` or document the `IEnumerable<T>`-only limit · Sev Med · Eff M
- [ ] C2 · Introduce a public `SimpleSettingsException` base; make boundary-crossing exceptions public + structured · Sev Med · Eff M · **Break**
- [x] C2 · Introduce a public `SimpleSettingsException` base; reparent all 10 exceptions, promote the 4 build-path escapees to public, flatten 3 to the root namespace, add leak-safe structured properties, retype the `TypeIsNotInterface` throw · Sev Med · Eff M · **Break**
- [x] C3 · Decide reload / provider-vs-singleton semantics (see also P1) · Sev High · Eff M–L · **DECIDED: option 2, provider-level cache (#17)**

**Phase 4 — Tests (write-first shortlist, then broaden)**
Expand Down Expand Up @@ -150,10 +151,14 @@ if (settingsOptions.AttributeType != null &&
**Fix — decide:** (a) document the limitation and throw a clear error, or (b) broaden `EnumerableTypeConverter.CanConvert` to handle assignable collection interfaces/`List<T>` and return a compatible instance. Coordinate with P4.
**Verify:** T6 doc test, upgraded to a positive test if (b).

### C2 · Public exception base + structured data — Sev Med · Eff M · **Break**
**Problem:** 9 exception types derive straight from `Exception` (no common base — can’t `catch (SimpleSettingsException)`); some are `internal` yet escape the public build path (uncatchable by type); context (binder/section/key/type) lives only in message strings; `TypeConverter.cs:62` throws a bare `Exception`.
**Fix:** add `public abstract class SimpleSettingsException : Exception`; reparent all library exceptions; make boundary-crossing ones public; expose context as properties; replace the bare `Exception` (ties into D-work). Add the standard ctor set.
**Verify:** builds; suite green; add a `catch (SimpleSettingsException)` test.
### C2 · Public exception base + structured data — Sev Med · Eff M · **Break** · **DONE (branch `refactor/c2-exception-hierarchy`)**
**Problem:** 10 exception types derived straight from `Exception` (no common base — couldn’t `catch (SimpleSettingsException)`); 4 were `internal` yet escaped the public build path (uncatchable by type); context (binder/section/key/type) lived only in message strings; the reachable not-an-interface guard threw an untyped `InvalidOperationException` (`SettingsCollection.cs:21,31`, `SettingsBuilder.cs:71`).

**Resolved (this PR).** Added `public abstract class SimpleSettingsException : Exception` (protected `(message)` + `(message, inner)` ctors; no parameterless/`[Serializable]` ctor — BinaryFormatter is obsolete on net8/10). Reparented all 10; promoted the 4 escapees (`SettingsPropertyValueException`, `SettingsPropertyNullException`, `TypeGenerationException`, `SettingsPropertyExtractionException`) to public; **flattened 3 mis-namespaced types** (`SettingsExtractionException` from `.Core`; `TypeGenerationException` + `SettingsPropertyExtractionException` from `.Core.Reflection`) to the root namespace so the public surface is coherent. Exposed leak-safe structured properties (`SettingsBindingException.{BinderType,Section,Key}`, `SettingsPropertyValueException.{SettingsType,PropertyName,TargetType,ConversionErrorType}`, `SettingsType`/`OptionType`/`ArgumentName` on the rest). New `SettingsTypeNotInterfaceException : SimpleSettingsException` replaces the 3 `InvalidOperationException(TypeIsNotInterface)` throws (the one real behavior break — flag in release notes). The two documented-**unreachable** "No converter found" `InvalidOperationException`s (`TypeConverter.cs:57`, `CollectionTypeConverter.cs:68`) were **left** as internal invariant guards.

**S1 preserved structurally:** `SettingsPropertyValueException`'s ctor now takes the failure's **`Type`** (not the `Exception`), so no value-bearing object crosses its boundary; `SettingsBindingException` stores primitives and **does not retain the `BindingContext`** (which holds the bound value). `InnerException == null` is asserted by test.

**Reviewed:** plan by `security-auditor` (ENDORSE-WITH-CHANGES — structural S1 hardening) + `dotnet-architect` (ENDORSE-WITH-CHANGES — namespace flattening, `Section`/`ConversionErrorType` naming, "now public" test must use reflection since IVT masks it); code by `/code-review` (high) + Roslyn `detect_antipatterns` (0). **Tests +6** (`SimpleSettings/ExceptionHierarchyTests.cs`): base is public+abstract; a reflection invariant that every library exception derives from the base; the 4 promotions are public (reflection); not-interface→typed+catchable; conversion→structured metadata + `InnerException == null`; binder-throws→context. Suite **82 net10** (was 76).

### C3 · Reload / provider-vs-singleton semantics — Sev High · Eff M–L
**Problem:** DI registers startup-built **singletons** (`ServicesSettingsBuilderExtensions.cs:37-42`), but `ISettingsProvider.GetSettings` **re-binds a fresh instance every call** (`SettingsBuilder.cs:99-108`) — so `GetService<IFoo>()` and `provider.GetSettings<IFoo>()` can return different objects/values. No `IOptionsMonitor`-style reload exists (acknowledged by the `// replace this…` TODO at `ServicesSettingsBuilderExtensions.cs:35`).
Expand Down
Loading
Loading