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
2 changes: 2 additions & 0 deletions .claude/agent-memory/atomic-executor/MEMORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Atomic Executor Memory Index

- [Nullable epic: pragma gate + analyzer restore](project_nullable_epic_pragma_gate_and_analyzer_restore.md) — per-file #nullable children: exact solution TWAE gate is blocked by pre-existing SVGControl CS0649 + UtilitiesCS CS0618/CS0168; use scoped `UtilitiesCS.csproj TWAE /p:WarningsNotAsErrors=CS0649%3BCS0618%3BCS0168`; restore mismatched analyzer pkgs (csproj 3.0.101 vs packages.config 3.0.123) into packages/ first

- [#349 breadcrumb WebView2 gotchas](project_349_breadcrumb_webview2_gotchas.md) — retyped Designer field breaks reflection-injected tests (inject a router instead); aggregate async d__ classes for >=90% proofs; QuickFiler.Test is Newtonsoft-free

- [VS18 build/test toolchain paths](project_vs18_build_toolchain_paths.md) — build with VS **18** full-framework msbuild.exe (not Core .dotnet-sdk, which dies on binary resx MSB3822); nuget.exe restore; MSYS_NO_PATHCONV; csharpier v1 subcommands; dotnet-coverage needs `--` separator
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: nullable-epic-pragma-gate-and-analyzer-restore
description: Per-file nullable-remediation epic children (#367 etc.) — the pragma-only TWAE solution build is blocked by pre-existing non-nullable debt; use a scoped genuine gate, and restore mismatched analyzer packages first
metadata:
type: project
---

Executing a `utilitiescs-nullable-remediation` epic child (per-file `#nullable enable` opt-in, NO project `<Nullable>`, pragma-only verification without `/p:Nullable=enable`). Learned on #367 (NewtonsoftHelpers, 19 files, 2026-07-19).

**Why:** the plan's exact type-check gate `msbuild TaskMaster.sln /t:Rebuild /p:TreatWarningsAsErrors=true` is structurally incapable of verifying the opted-in files: it fails on pre-existing non-nullable warnings promoted by TWAE and never even compiles the target assembly.

**How to apply (for the remaining epic children too):**
- Two pre-existing blockers under TWAE (no Nullable=enable), both on origin/main, unrelated to the nullable work: (1) vendored `SVGControl/SvgImageSelector.cs` 2x `CS0649` — and UtilitiesCS depends on SVGControl, so the whole solution halts before UtilitiesCS compiles; (2) `UtilitiesCS` itself has ~14x `CS0618` (obsolete System.Linq.AsyncEnumerable) + 1x `CS0168`. Test projects add pre-existing `CS8632`/`CS0067`/`CS0169`/`CS2002`.
- GENUINE per-file nullable gate that actually compiles the code and keeps CS86xx fatal: `msbuild <Proj>/<Proj>.csproj /t:Rebuild /p:Configuration=Debug /p:Platform=AnyCPU /p:TreatWarningsAsErrors=true /p:WarningsNotAsErrors=CS0649%3BCS0618%3BCS0168 /m` (semicolons MUST be `%3B`-escaped or msbuild throws MSB1006). EXIT 0 == zero CS86xx in the opted-in files. Also grep the compile log for `CS86` in the target folder path as a direct measure. Still run the exact plan solution command for the record (it stays invariant: only the 2 SVGControl CS0649; 0 CS86xx).
- Analyzer-package version mismatch is PRE-EXISTING on origin/main and blocks ALL builds (CS0006): every first-party `.csproj` `<Analyzer Include>` references Meziantou.Analyzer 3.0.101 / SonarAnalyzer.CSharp 10.27.0.140913 / Microsoft.CodeAnalysis.BannedApiAnalyzers 3.3.4, but `packages.config` (and the Meziantou `<Import>`) declare 3.0.123 / 10.29.0.143774 / 5.6.0. `nuget restore` only fetches packages.config versions. FIX (env bootstrap, no tracked-file edit — packages/ is gitignored): `nuget.exe install <id> -Version <old> -OutputDirectory packages` for those three. Do NOT edit the csprojs.
- `#nullable enable` goes at file top; files carry a UTF-8 BOM — the Edit tool preserves it (match `using System;`, BOM stays at byte 0). csharpier v1 reorders `override sealed`->`sealed override` and rebreaks long converter signatures — always run `csharpier format` on touched files then re-run the gate.
- Framework nullability to MATCH (Newtonsoft.Json 13.0.x embeds NRT metadata even in lib/net45): converter `existingValue`/`value`/`ReadJson` return are the nullable positions; `reader`/`writer`/`serializer`/`objectType` stay non-null. `WriteJson value` becomes `T?`/`object?` but the body's `value.GetType()`/`.ToComposition(value)`/`.FolderPath` derefs need behavior-preserving `value!` (Newtonsoft passes non-null for a registered converter). `ISerializationBinder.BindToType` return must STAY non-null `Type` with `!` (a `Type?` return is CS8766); `BindToName` out is `string?`. `ITraceWriter.Trace` is `Exception? ex`. log4net/Mono.Reflection are oblivious.
- `[JsonProperty] object RemainingObject` and DTO deserialization targets: use `= null!` (preserve non-null contract; the code guards with `ThrowIfNull()`), not `object?` — `ThrowIfNull()` returns non-null but called as a statement it does NOT narrow the receiver. `ILInstruction.Operand` becomes `object?`, so wrapper reflection casts `(FieldInfo)/(MethodInfo)instruction.Operand` need `!`. Per-file `ModifySetMethod` return nullability differs (WrapperScDictionary throws=non-null; WrapperSco/People `return null`=`MethodBuilder?`) — do NOT unify.
- Coverage: `Invoke-MSTestWithCoverage.ps1` throws `property 'Count' not found` under StrictMode when only ONE `*.Test.dll` is in scope; use `dotnet-coverage collect --output OUT --output-format cobertura --settings coverage.config -- "$VSTEST" UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll /Settings:scripts/vscode/TaskMaster.cli.runsettings /InIsolation /TestCaseFilter:TestCategory!=LiveOutlook`. NewtonsoftHelpers production coverage ~93.7%; annotation-only edits do not regress it.
1 change: 1 addition & 0 deletions .claude/agent-memory/feature-review/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@
- [SVGControl stale binding redirect out of scope](project_svgcontrol-stale-binding-redirect-out-of-scope.md) — #354: `SVGControl/app.config`'s `System.Runtime.CompilerServices.Unsafe` redirect (6.0.2.0 vs csproj 6.0.3.0) is still stale as of 2026-07-18; check it in any future app.config binding-redirect audit
- [pr-context stale after remediation commit](project_pr-context-stale-after-remediation-commit.md) — #354 R4: pr_context artifacts don't auto-update after a remediation commit lands; compare `git rev-parse HEAD` against the summary's recorded Head ref every re-audit cycle and refresh if stale
- [partial remediation still fails new-code floor](project_partial-remediation-new-code-floor-still-fails-209.md) — #209 R4: extracting one testable seam from a native-engine adapter raised coverage 0%->7.7%, real progress but still far below 85%/90%; recompute against the actual floor, don't trust a remediation-plan's weak ">0%" acceptance bar; recommend a maintainer exemption decision when the residual is architecturally irreducible
- [coverage hook skips when no pr_context.summary.txt](coverage-hook-skips-when-no-pr-context-summary.md) — #367: absent pr_context summary makes changedLanguages empty, so the hook runs only the 3 artifact-path/location checks and skips all per-language coverage-row enforcement; still write clean PASS/FAIL rows; evidence/qa-gates/ is an allowed audit-artifact path
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
name: coverage-hook-skips-when-no-pr-context-summary
description: TaskMaster validate-feature-review-coverage.ps1 skips ALL per-language coverage-row checks when artifacts/pr_context.summary.txt is absent
metadata:
type: project
---

TaskMaster's `validate-feature-review-coverage.ps1` (SubagentStop hook) computes the changed-language set solely from `artifacts/pr_context.summary.txt` via `Get-ChangedLanguageSet`. If that file is absent, `$changedLanguages.Count -eq 0` and the function returns `Ok=true` BEFORE running any per-language coverage-row PASS/FAIL / narrowing-phrase check (line ~426-428). Only the three artifact-path-advertisement + canonical-location + matching-folder/timestamp checks still run.

**Why:** #367 (epic child, base = epic integration branch) had no pr_context artifacts in-session, so the C#-coverage-row enforcement never fired even though 19 `.cs` files changed.

**How to apply:** When no `artifacts/pr_context.summary.txt` exists, the hook will not block on a missing/narrowed C# coverage verdict — but the feature-review policy still requires explicit per-language coverage verdicts, so write a clean `PASS`/`FAIL` coverage row anyway. Also note: `Get-LanguageRepoCoverage` for C# reads `artifacts/csharp/coverage.xml`; when that file is intentionally NOT emitted (annotation-only epic children where the fixed 85% floor would false-FAIL against a pre-existing repo-wide %), the C# repo pct resolves `$null` and no forced-FAIL occurs. Writing audit artifacts to `docs/features/active/<f>/evidence/qa-gates/` is NOT a forbidden path (enforce-evidence-locations only blocks `artifacts/*` prefixes), and the coverage hook's path regex accepts the nested `evidence/qa-gates/` folder since its Folder group is `.+`. See [[csharp-coverage-artifact-is-cobertura]], [[pr-context-mcp-unavailable-manual-fallback]].
8 changes: 6 additions & 2 deletions UtilitiesCS/NewtonsoftHelpers/AllInclusiveBinder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand All @@ -9,7 +10,10 @@ namespace UtilitiesCS.NewtonsoftHelpers
{
public class AllInclusiveBinder
{
public Assembly[] GetAssemblies()
// Deliberate contract decision: this is an unused stub whose body returns null,
// so the return type is annotated Assembly[]? to reflect the actual null behavior
// (plain class, no ISerializationBinder constraint to satisfy).
public Assembly[]? GetAssemblies()
{
//var dataAssembly = typeof(AnClassInDataLayer).Assembly;
//var businessAssembly = typeof(ACLassInBusinessLayer).Assembly;
Expand Down
8 changes: 5 additions & 3 deletions UtilitiesCS/NewtonsoftHelpers/AppGlobalsConverter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
Expand All @@ -23,17 +24,18 @@ public AppGlobalsConverter(IApplicationGlobals globals)
public override IApplicationGlobals ReadJson(
JsonReader reader,
Type objectType,
IApplicationGlobals existingValue,
IApplicationGlobals? existingValue,
bool hasExistingValue,
JsonSerializer serializer
)
{
// Deliberate non-null return: the body always returns the ctor-injected _globals.
return _globals;
}

public override void WriteJson(
JsonWriter writer,
IApplicationGlobals value,
IApplicationGlobals? value,
JsonSerializer serializer
)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -13,10 +14,14 @@ namespace UtilitiesCS.NewtonsoftHelpers
public class DerivedCompositionConverter_ConcurrentDictionary<TDerived, TKey, TValue>
where TDerived : ConcurrentDictionary<TKey, TValue>
{
public ConcurrentDictionary<TKey, TValue> ConcurrentDictionary { get; set; }
public object RemainingObject { get; set; }
public Dictionary<string, object> AdditionalFields { get; private set; }
public Dictionary<string, object> AdditionalProperties { get; private set; }
// These are populated by ToComposition/ToCompositionOld before the derived-conversion
// members read them; annotated null! to preserve the existing non-null contract without
// adding guards on every consumer. The additional-* dictionaries hold reflection values,
// which can legitimately be null, so their value type is widened to object?.
public ConcurrentDictionary<TKey, TValue> ConcurrentDictionary { get; set; } = null!;
public object RemainingObject { get; set; } = null!;
public Dictionary<string, object?> AdditionalFields { get; private set; } = null!;
public Dictionary<string, object?> AdditionalProperties { get; private set; } = null!;

public DerivedCompositionConverter_ConcurrentDictionary() { }

Expand Down Expand Up @@ -64,7 +69,7 @@ public DerivedCompositionConverter_ConcurrentDictionary<
public TDerived ToDerivedOld()
{
// Create an instance using reflection
var derivedInstance = (TDerived)Activator.CreateInstance(typeof(TDerived), true);
var derivedInstance = (TDerived)Activator.CreateInstance(typeof(TDerived), true)!;

// Copy dictionary entries
foreach (var kvp in ConcurrentDictionary)
Expand Down Expand Up @@ -165,13 +170,13 @@ public Type EmitNewClass()
}
}

return typeBuilder.CreateTypeInfo().AsType();
return typeBuilder.CreateTypeInfo()!.AsType();
}

public object ConvertToNewClassInstance(TDerived derivedInstance)
{
var newClassType = EmitNewClass();
var newClassInstance = Activator.CreateInstance(newClassType);
var newClassInstance = Activator.CreateInstance(newClassType)!;

var derivedType = typeof(TDerived);
var baseType = typeof(ConcurrentDictionary<TKey, TValue>);
Expand Down
22 changes: 13 additions & 9 deletions UtilitiesCS/NewtonsoftHelpers/FilePathHelperConverter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -16,7 +17,9 @@ public FilePathHelperConverter(IFileSystemFolderPaths fileSystemFolders)
FileSystemFolders = fileSystemFolders;
}

protected IFileSystemFolderPaths _fileSystemFolders;
// null! preserves the non-null contract: the public ctor sets this from its required
// (non-null) dependency; the protected parameterless ctor exists for subclassing/testing.
protected IFileSystemFolderPaths _fileSystemFolders = null!;
internal virtual IFileSystemFolderPaths FileSystemFolders
{
get => _fileSystemFolders;
Expand All @@ -26,7 +29,7 @@ internal virtual IFileSystemFolderPaths FileSystemFolders
public override FilePathHelper ReadJson(
JsonReader reader,
Type objectType,
FilePathHelper existingValue,
FilePathHelper? existingValue,
bool hasExistingValue,
JsonSerializer serializer
)
Expand All @@ -38,7 +41,7 @@ JsonSerializer serializer
return new FilePathHelper(fileName, folderPath);
}

internal string ExtractFolderPath(Dictionary<string, string> info)
internal string? ExtractFolderPath(Dictionary<string, string> info)
{
if (!info.TryGetValue("SpecialFolderName", out string folderName))
{
Expand Down Expand Up @@ -139,9 +142,8 @@ private static string GetErrorMessage(JsonReader reader)
{
var message =
$"{nameof(FilePathHelperConverter)}.{nameof(ReadJson)} encountered a problem";
if (reader is JsonTextReader)
if (reader is JsonTextReader textReader)
{
var textReader = reader as JsonTextReader;
message += $" on line {textReader.LineNumber} ({reader.Path}).";
}
return message;
Expand Down Expand Up @@ -194,16 +196,18 @@ internal string ReadPropertyValue(JsonReader reader)

public override void WriteJson(
JsonWriter writer,
FilePathHelper value,
FilePathHelper? value,
JsonSerializer serializer
)
{
var (name, relativePath) = GetSerializablePath(value.FolderPath);
// value! preserves behavior: Newtonsoft invokes WriteJson with a non-null value for
// a registered converter.
var (name, relativePath) = GetSerializablePath(value!.FolderPath);

writer.WriteStartObject();

writer.WritePropertyName("FileName");
writer.WriteValue(value.FileName);
writer.WriteValue(value!.FileName);

writer.WritePropertyName("RelativePath");
writer.WriteValue(relativePath);
Expand Down
17 changes: 12 additions & 5 deletions UtilitiesCS/NewtonsoftHelpers/KnownTypesBinder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
Expand All @@ -9,14 +10,20 @@ namespace UtilitiesCS.NewtonsoftHelpers
{
public class KnownTypesBinder : ISerializationBinder
{
public IList<Type> KnownTypes { get; set; }
// Caller-populated public setter; nullable is the honest annotation (it is not
// initialized by this type).
public IList<Type>? KnownTypes { get; set; }

public Type BindToType(string assemblyName, string typeName)
public Type BindToType(string? assemblyName, string typeName)
{
return KnownTypes.SingleOrDefault(t => t.Name == typeName);
// ISerializationBinder.BindToType declares a NON-null Type return, so the
// contract cannot become Type? (that would be CS8766). The body returns
// SingleOrDefault(...) which is null on no match; ! preserves the existing
// runtime behavior (Newtonsoft tolerates a null return via default binding).
return KnownTypes!.SingleOrDefault(t => t.Name == typeName)!;
}

public void BindToName(Type serializedType, out string assemblyName, out string typeName)
public void BindToName(Type serializedType, out string? assemblyName, out string typeName)
{
assemblyName = null;
typeName = serializedType.Name;
Expand Down
3 changes: 2 additions & 1 deletion UtilitiesCS/NewtonsoftHelpers/MonoExtension/MonoExtension.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand Down
9 changes: 6 additions & 3 deletions UtilitiesCS/NewtonsoftHelpers/NConsoleTraceWriter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand All @@ -25,9 +26,11 @@ public List<string> MessageFilter
set => _messageFilter = value;
}

public Action<string, Exception> Log { get; set; }
// Deliberate public contract: the log delegate is optional (may be unset) and its
// exception argument is nullable, matching ITraceWriter.Trace(..., Exception? ex).
public Action<string, Exception?>? Log { get; set; }

public void Trace(TraceLevel level, string message, Exception ex)
public void Trace(TraceLevel level, string message, Exception? ex)
{
if (!MessageFilter.Select(message.Contains).Aggregate((a, b) => a | b))
{
Expand Down
11 changes: 7 additions & 4 deletions UtilitiesCS/NewtonsoftHelpers/NLogTraceWriter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
Expand All @@ -9,7 +10,9 @@
public class NLogTraceWriter : ITraceWriter
{
private static readonly log4net.ILog _logger = log4net.LogManager.GetLogger(
System.Reflection.MethodBase.GetCurrentMethod().DeclaringType
// ! preserves behavior: GetCurrentMethod() is non-null in this static initializer
// and its DeclaringType is the enclosing type.
System.Reflection.MethodBase.GetCurrentMethod()!.DeclaringType!
);

internal virtual ILog Logger => _logger;
Expand All @@ -28,7 +31,7 @@ public List<string> MessageFilter
set => _messageFilter = value;
}

public void Trace(TraceLevel level, string message, Exception ex)
public void Trace(TraceLevel level, string message, Exception? ex)
{
if (!MessageFilter.Select(message.Contains).Aggregate((a, b) => a | b))
{
Expand All @@ -37,7 +40,7 @@ public void Trace(TraceLevel level, string message, Exception ex)
}
}

private Action<string, Exception> GetLogFunction(TraceLevel level)
private Action<string, Exception?>? GetLogFunction(TraceLevel level)
{
switch (level)
{
Expand Down
6 changes: 3 additions & 3 deletions UtilitiesCS/NewtonsoftHelpers/NonRecursiveConverter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using Newtonsoft.Json;

/// <summary>
Expand All @@ -24,9 +25,8 @@ public abstract class NonRecursiveConverter<TConverter> : JsonConverter
/// <inheritdoc/>
public override bool CanWrite => !s_writing;

#nullable enable
/// <inheritdoc/>
public override sealed object? ReadJson(
public sealed override object? ReadJson(
JsonReader reader,
Type objectType,
object? existingValue,
Expand Down
Loading