Skip to content

fix(SVGControl): stop SvgRenderer surfacing parse failures as NullReferenceException - #421

Merged
drmoisan merged 16 commits into
mainfrom
bug/svg-renderer-null-document-nre-418
Aug 6, 2026
Merged

fix(SVGControl): stop SvgRenderer surfacing parse failures as NullReferenceException#421
drmoisan merged 16 commits into
mainfrom
bug/svg-renderer-null-document-nre-418

Conversation

@drmoisan

@drmoisan drmoisan commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Suggested title

fix(SVGControl): stop SvgRenderer surfacing parse failures as NullReferenceException

Summary

  • SvgRenderer.GetSvgDocument(byte[]) swallowed every parse exception and returned null; both byte-array constructors dereferenced that result immediately, so any parse failure surfaced as an opaque NullReferenceException at control-construction time.
  • The byte-array constructors now degrade visibly instead of throwing — logging the cause through log4net and System.Diagnostics.Trace, then leaving _doc null without dereferencing it.
  • A fail-fast surface is added for callers that want one: TryGetSvgDocument and GetSvgDocumentOrThrow. GetSvgDocument keeps its tolerant null-returning contract, so existing null-tolerant call sites are unchanged.
  • The AssemblyResolve fallback gains ordered directory probing, and its exception containment is restored so it cannot throw out of the handler.
  • The AssemblyResolve region and two pure path helpers move to SVGControl/SvgAssemblyResolver.cs and SVGControl/SvgAssemblyProbe.cs; SvgRenderer.cs drops from 497 to 362 lines.
  • SVGControl.Test is wired into TaskMaster.sln for the first time and gains 27 tests. Suite: 6150/6150 across 9 assemblies.

Why

PictureBoxSVG is constructed by designer-generated code in eleven forms, including one that runs inside the Outlook add-in. Opening any of them in the WinForms designer failed to load, and because GetSvgDocument discarded the originating exception, the cause was undiagnosable at every call site.

The root cause is a binding mismatch: the deployed Svg assembly carries a reference to an ExCSS version that is not the one deployed, so SvgDocument.Open throws FileNotFoundException in hosts that do not apply the project app.config binding redirects. devenv.exe is such a host. A pre-existing AssemblyResolve fallback was reached but returned null, because Assembly.Load binds against the host AppDomain's ApplicationBase — the Visual Studio directory — rather than the directory holding SVGControl.dll.

Two constraints shaped the fix, both recorded in docs/features/active/2026-08-04-svg-renderer-null-document-nre-418/issue.md:

  • The constructors must not throw. Throwing would convert a blank-icon degradation into a control-construction failure for end users in the add-in. Degrade-and-log was chosen deliberately over fail-fast at that call site.
  • The diagnostic must reach a channel the designer surfaces. SVGControl declares a log4net logger, but there is no evidence an appender is configured inside devenv.exe, so a log4net-only message could surface nowhere an operator can see it. Hence the dual channel.

What Changed

Core fix — SVGControl/SvgRenderer.cs

  • GetSvgDocument(byte[]) no longer contains try/catch. It delegates and returns SvgDocument?, preserving the tolerant contract its existing consumers rely on.
  • New TryGetSvgDocument (public, plus an internal seam-bearing overload taking a parse delegate) and GetSvgDocumentOrThrow, which throws InvalidOperationException carrying the original exception as InnerException.
  • Both byte-array constructors log through log4net and Trace, then initialise safely without dereferencing a null document.
  • A bare catch { } was removed.

Assembly resolution — new files

  • SVGControl/SvgAssemblyResolver.cs holds the static installer, the re-entrancy guard, ResolveByNameAndKey, and the public-key-token comparison. SvgRenderer's static constructor is retained with its body reduced to SvgAssemblyResolver.Install().
  • SVGControl/SvgAssemblyProbe.cs holds TryGetDirectoryFromCodeBase and GetProbeDirectories, both pure path-string functions.
  • ResolveByNameAndKey gains a strategy-3 Assembly.LoadFrom probe over ordered candidate directories, plus a catch on its outer try so no exception escapes the handler. The invalid-path-character filter applies to all three probe candidates.

Tests and build configuration

  • SVGControl.Test added to TaskMaster.sln with all six configuration mappings.
  • Three new test files: SvgRendererParseContractTests.cs, SvgRendererNullToleranceTests.cs, SvgAssemblyProbeDirectoryTests.cs.
  • Svg and ExCSS <Reference> items plus packages.config entries. A non-SDK ProjectReference does not flow the referenced project's package assemblies, so both were required explicitly.
  • SVGControl.Test/app.config ExCSS binding redirect corrected to the deployed version.
  • <LangVersion>latest</LangVersion> added to SVGControl.Test, eliminating a CS8630 that blocked nullable analysis for that project.

Architecture / How It Fits Together

PictureBoxSVGSvgImageSelectorSvgRenderer(byte[], …) is the designer-time construction path. The renderer now has three parse entry points over one shared implementation:

  • GetSvgDocument — tolerant, returns null, used by existing consumers.
  • TryGetSvgDocument — reports failure explicitly and surfaces the captured exception. The internal overload accepts a Func<byte[], SvgDocument?> parse delegate, which is the seam the tests use to drive the null-returning branch deterministically.
  • GetSvgDocumentOrThrow — throws with the original exception attached.

SvgAssemblyResolver.Install() is triggered once from SvgRenderer's static constructor and subscribes ResolveByNameAndKey to AppDomain.CurrentDomain.AssemblyResolve. That handler delegates candidate-directory derivation to SvgAssemblyProbe, which never raises, so the handler cannot fail the bind it is trying to rescue.

Verification

Completed

Recorded under docs/features/active/2026-08-04-svg-renderer-null-document-nre-418/evidence/:

Gate Result Evidence
Format csharpier check exit 0 qa-gates/csharpier-check.2026-08-05T05-00.md
Analyzers exit 0, 0 errors, no new diagnostics qa-gates/analyzer-build.2026-08-05T05-00.md
Nullable / TWAE exit 0, 0 errors qa-gates/nullable-build.2026-08-05T05-00.md
Tests 6150/6150 across 9 assemblies qa-gates/test-coverage.2026-08-05T05-00.md
Single clean pass recorded qa-gates/toolchain-clean-pass.2026-08-05T05-00.md
Coverage repo line 85.4006%, branch 78.6928% qa-gates/coverage-delta.2026-08-05T05-00.md
  • Fail-before / pass-after. The regression tests were captured failing before the fix (regression-testing/ac1-fail-before.2026-08-04T14-36.md) and passing after (regression-testing/ac1-pass-after.2026-08-04T14-36.md).
  • Test-order independence. SVGControl.Test previously passed or failed depending on vstest argument order, because its output lacked ExCSS.dll. Standalone it went from 6 failures to 75/75/0. See qa-gates/order-independence.2026-08-05T05-00.md, regression-testing/order-standalone-after.2026-08-05T05-00.md, and regression-testing/order-paired-after.2026-08-05T05-00.md.
  • Designer load, human-verified. The form opens in the Visual Studio WinForms designer, renders correctly, and shows no NullReferenceException. See regression-testing/designer-load-2026-08-06T19-47.md.

Recommended

dotnet tool run csharpier check .
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-VSBuild.ps1 -SolutionPath TaskMaster.sln -Configuration Debug -Platform "Any CPU" -EnableNETAnalyzers -EnforceCodeStyleInBuild
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-VSBuild.ps1 -SolutionPath TaskMaster.sln -Configuration Debug -Platform "Any CPU" -EnableNullable -TreatWarningsAsErrors
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug

Backward Compatibility / Migration Notes

  • No breaking change to existing callers. GetSvgDocument keeps its signature and its tolerant null-returning behavior. The new members are additive.
  • Behavior change, intended: the byte-array constructors previously threw NullReferenceException on a parse failure and now degrade to a null document with a logged diagnostic. Callers that relied on the exception — none found in-repo — would need to inspect Document.
  • SVGControl.Test becomes a solution member, so it now builds and runs in solution-wide operations for the first time.
  • No public type was renamed or removed. ResolveByNameAndKey moved file but remains private static on a type in the same namespace.

Risks and Mitigations

Risk Mitigation
A parse failure now degrades silently to a blank icon rather than failing loudly Dual-channel diagnostic on every failure path; GetSvgDocumentOrThrow available where fail-fast is wanted
The relocated AssemblyResolve handler could stop installing SvgRenderer's static constructor deliberately retained; Install() measures 6/6 = 100% coverage, so the trigger is exercised
New probing could throw inside an AssemblyResolve handler Exception containment restored on the outer try; SvgAssemblyProbe documented and tested as never-raising, at 100% line and branch
SVGControl.Test entering the solution changes solution-wide build and coverage Verified: analyzer and nullable builds exit 0 with no new diagnostics; repo coverage rose on both axes

Rollback is a straight revert of the branch; no data migration and no configuration change outside SVGControl.Test.

Review Guide

Suggested order:

  1. SVGControl/SvgRenderer.cs — the substantive change. Read GetSvgDocument, the two TryGetSvgDocument overloads, GetSvgDocumentOrThrow, and both byte-array constructors.
  2. SVGControl/SvgAssemblyResolver.cs and SVGControl/SvgAssemblyProbe.cs — largely a move out of SvgRenderer.cs; the genuinely new logic is strategy-3 probing and the containment catch.
  3. The three test files.
  4. Build configuration: SVGControl.Test.csproj, packages.config, app.config, TaskMaster.sln, SVGControl.csproj.

Noise to expect: the diff is 164 files, but only 11 are code or build configuration (1245 insertions). The remainder is the feature folder — plan, research, audits, and evidence artifacts. The PR-context bundle's "Core logic changes: 0 files" line is a known misclassification by that collector, not a description of this branch.

Follow-ups

Filed rather than folded in, under docs/features/potential/:

  • 2026-08-04-invoke-mstest-scalar-count-strictmode.mdInvoke-MSTest.ps1 throws on a single-assembly search root under Set-StrictMode. Kept out because a PowerShell change would pull a second toolchain into a C#-only fix.
  • 2026-08-04-stale-fizzler-and-unsafe-binding-redirects.md — twelve Fizzler redirects and one Unsafe redirect name versions that are not deployed. Latent today; same defect class as this issue.
  • 2026-08-05-svgcontrol-coverage-uplift.md — residual SVGControl coverage, including SvgAssemblyResolver.cs.

Two items carry a maintainer decision, both recorded in issue.md:

  • A coverage threshold exception for SVGControl/SvgAssemblyResolver.cs, whose shortfall is entirely a CLR-invoked AssemblyResolve handler that cannot be unit-tested without staging an assembly on disk. It is a threshold exception, not a measurement exclusion — no [ExcludeFromCodeCoverage] was added and the lines remain in the repo-wide denominator.
  • The designer-load capture states its own limits: Visual Studio was not restarted after the build, so attributing the successful bind specifically to this fix is conditional rather than proven, and open question U-2 is untested.

GitHub Auto-close

drmoisan and others added 16 commits August 4, 2026 16:53
- Add the active feature folder for the SvgRenderer null-document NRE: issue with AC-1..AC-11, atomic plan v0.3, root-cause research, WinForms designer verification runbook, and Phase 0 baseline evidence
- Register SVGControl.Test in TaskMaster.sln with Debug/Release configurations for Any CPU, x64, and x86
- Correct the ExCSS binding redirect in SVGControl.Test/app.config from the non-existent 4.2.4.0 to 4.3.1.0
- Restore the Svg 3.4.7 package pin and add the compile-time Svg reference so SVGControl.Test builds
- Record agent-memory entries for legacy csproj compile references, missing VSTO runtime effects on baseline gates, and new solution-member pin divergence
- No production C# is modified; SVGControl/SvgRenderer.cs is unchanged and execution is paused at plan task P1-T6 with a SCOPE_EXCEEDED escalation pending a host with the VSTO runtime assemblies

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Record the resume point: execution paused at plan task P1-T6 with SCOPE_EXCEEDED on an MSB3277 System.Runtime.CompilerServices.Unsafe pin divergence in SVGControl.Test
- Reproduce the authored-but-unapplied atomic-planner Scope Lock delta verbatim, including the new P1-T6a pin-alignment task and Design Decision 10
- Instruct the receiving orchestrator to re-run Phase 0 baseline capture, since every committed baseline was taken on a host lacking the VSTO runtime assemblies and is invalid elsewhere
- Instruct reverting the AC-6 relative-measure amendment and dropping human_interaction requirement H-3, both of which are specific to the originating host
- Supply full orchestrator-state reconstruction values, since artifacts/ is gitignored and the checkpoint does not travel with the branch
- Carry forward the confirmed root cause, the settled design decisions D1-D3, and the explicit out-of-scope list

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Capture seven Phase 0 baseline evidence artifacts in the 2026-08-04T21-04
  series. The committed 14-36 set was invalid twice over: captured on a host
  lacking the VSTO runtime assemblies, and against a pre-package-update
  dependency graph. The new baseline records the analyzer and nullable builds
  each returning EXIT_CODE 0, zero CS0234/MSB3245, MSB3277 count 0, nine test
  assemblies with 6112/6112 passing, and repo coverage 85.3550% line /
  78.5353% branch. The 14-36 artifacts are retained for audit.
- Revise the plan in place from v0.3 to v0.6 across three revision passes.
  Design Decision 10 records that the System.Runtime.CompilerServices.Unsafe
  pin divergence which halted execution at P1-T6 was resolved upstream by the
  rebase onto ce0c91e, not by a plan task, so no P1-T6a was added. Design
  Decision 11 makes nullable annotations mandatory on the new SvgRenderer
  surface, because the file is #nullable enable and net48 has no NotNullWhen.
  Split P2-T8's coverage gate into a newly-added >= 90% set and a changed-member
  no-regression set to match AC-5 rather than exceed it, with a named exception
  for the CLR-invoked ResolveByNameAndKey. Retarget every Phase 0 baseline
  reference to the new series and correct stale version literals.
- Revert AC-6 to the absolute single-clean-pass form; the relative amendment was
  specific to the host without the VSTO runtime. Amend AC-9 and AC-10 to record
  the package versions superseded by PR #419.
- Record four agent-memory findings: incremental builds yielding a vacuous
  nullable baseline, coverage gates on CLR-invoked private members, the nullable
  context mismatch between production and test projects, and the planner tool
  surface excluding the MCP validator.

No production or test C# is modified; SVGControl/SvgRenderer.cs is untouched.
Preflight returns ALL CLEAR against v0.6; execution resumes at P1-T6.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nstructors

- GetSvgDocument no longer contains a try/catch. It delegates and keeps its
  tolerant SvgDocument?-returning contract, so all six existing null-tolerant
  call sites are unaffected.
- Add a fail-fast surface for callers that want one: TryGetSvgDocument (public
  3-arg, plus an internal 4-arg seam-bearing overload) and GetSvgDocumentOrThrow,
  which throws with the original exception as InnerException.
- Both byte-array constructors now degrade visibly rather than throwing. They log
  through the existing log4net logger and through System.Diagnostics.Trace, so
  the diagnostic is visible in the WinForms designer host where no log4net
  appender is configured, then leave _doc null without dereferencing it.
- ResolveByNameAndKey gains ordered directory probing via Assembly.LoadFrom.
  The previous fallback returned null because Assembly.Load bound against the
  host AppDomain's ApplicationBase - the Visual Studio directory - rather than
  the directory holding SVGControl.dll. The bare catch was also replaced.
- Extract the two pure assembly-probe helpers, TryGetDirectoryFromCodeBase and
  GetProbeDirectories, into SVGControl/SvgAssemblyProbe.cs. SvgRenderer.cs had
  reached 547 lines against the repository's 500-line limit, and a tightening
  pass could not close the gap without breaking acceptance clauses that require
  the log4net and Trace calls to appear literally in the catch block and in both
  constructors. The cut is correct on cohesion grounds independently of the line
  count: both helpers are pure path-string functions with no renderer-state
  dependency. SvgRenderer.cs is now 495 lines and SvgAssemblyProbe.cs is 67.
  Wired in via an explicit <Compile Include> since SVGControl is legacy non-SDK.
- Add 27 tests in SVGControl.Test/SvgRendererParseContractTests.cs. The suite is
  6139/6139 passing across nine assemblies, up from a 6112 baseline. Four are the
  AC-1 regression tests, captured failing before the fix and passing after.

Corrects a factual error the tests exposed: the plan and AC-5's note both claimed
Array.Empty<byte>() reaches an element-free path where SvgDocument.Open returns
null without throwing. It actually raises XmlException "Root element is missing".
The affected tests now assert the measured behavior, which is strictly stronger
than the original assertion, and the element-free branch is driven through the
injected parse delegate instead. AC-5's note still carries the false claim and is
corrected before Phase 2 checks AC-5 off.

Acceptance criteria checked off: AC-1, AC-2, AC-3, AC-4, AC-7, AC-8, AC-9, AC-10.
AC-5 and AC-6 belong to Phase 2. AC-11 needs a human designer-load verification
and remains unchecked by design.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Add GetSvgDocumentOrThrow_WithTheBuiltInDefaultImage_ReturnsADocument. The
  member had two throwing-path tests and no success-path test, so its
  `return document!;` line was uncovered and it measured ~67% against the 90%
  new-member gate. It now measures 100%.
- Correct two comments that asserted a claim Phase 1 disproved - that
  element-free input makes SvgDocument.Open return null without throwing. The
  production comment on OpenFromBytes and the class-level XML doc in the test
  file now record that the null-returning path is reached through the injected
  parse delegate and that an empty payload raises XmlException. The replacement
  used 4 of a 7-line budget, so SvgRenderer.cs went 495 to 497 against the
  500-line limit.

One consecutive clean toolchain pass, no restart:
- csharpier format reformatted zero files; csharpier check exits 0.
- Analyzer build: 0 errors, 6 warnings, no new diagnostics vs baseline.
- Nullable/TreatWarningsAsErrors: 0 errors, 5 warnings, zero CS86xx.
- Tests: 6140/6140 passing across 9 assemblies, up from a 6112 baseline.

Coverage:
- All seven newly-added members measure 100% line rate against the 90% gate.
- Changed members improved: GetSvgDocument 62.5% to 100%, and the
  SvgRenderer(byte[], Size, AutoSize) constructor 0% to 76.5%.
- The SvgRenderer class went 62.559% to 72.109%, +9.55 points and +160 lines.
- Repo-wide line 85.3550% to 85.3844%, branch 78.5353% to 78.5521%; both above
  their floors and both improved, so no denominator-change fallback was needed.
- ResolveByNameAndKey's rate fell 72.09% to 68.12% while its covered lines rose
  31 to 47. That is a denominator effect from the new strategy-3 assembly-probe
  wiring, which carries the ratified named coverage exception.

AC-5 (coverage on changed code) and AC-6 (toolchain passes in a single clean
pass) are checked off, bringing delivered acceptance criteria to ten of eleven.
AC-11 requires a human to verify the WinForms designer load against
runbooks/verify-winforms-designer-load.runbook.md and remains unchecked by
design; the handoff is recorded in evidence/other/.

All 46 plan tasks are complete.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lver

Remediation cycle 1 for the five non-blocking findings from feature review.
All 40 remediation-plan tasks complete.

- Extract the AssemblyResolve region - static installer, re-entrancy guard,
  ResolveByNameAndKey, and the public-key-token comparison - into the new
  SVGControl/SvgAssemblyResolver.cs (157 lines). SvgRenderer.cs drops 497 to
  362, relieving pressure against the 500-line limit. SvgRenderer's static
  constructor is deliberately RETAINED with its body reduced to
  SvgAssemblyResolver.Install(); moving it wholesale would have silently
  disabled the resolver while leaving the build green and the tests passing.
  Install() measures 100% coverage, so the resolver is proven to still install.
- Restore exception containment: a catch (Exception ex) on the outer try/finally,
  and the invalid-path-character filter now applied to the third probe candidate
  as well as the first two. Previously Path.Combine, Assembly.Location, and
  Assembly.CodeBase could throw out of an AssemblyResolve handler, converting a
  recoverable bind failure into the opaque construction-time failure this issue
  exists to eliminate - and contradicting SvgAssemblyProbe's own documented
  "never raises, so it is safe inside an AssemblyResolve handler" contract.
- Relocate PublicKeyTokensEqual to SvgAssemblyProbe and cover it: 0/15 to 15/15
  line, 18/18 branch. AC-8 requires its behavior be preserved and nothing
  tested it. Eight cases are required rather than seven: the expression has ten
  condition outcomes and only (non-empty, null) drives the last two.
- Correct two stale comments: the header block that was the sole in-code
  explanation for the fallback named package versions no longer present in the
  repository, so a reader could have concluded the fallback was dead code and
  removed working error handling; and one test comment overstated a claim
  beyond what was measured.

Add <LangVersion>latest</LangVersion> to SVGControl.Test, eliminating CS8630.
The measurement contradicted the prediction: Roslyn suppresses nullable
diagnostics in generated code, so the *.Designer.cs diagnostics that were
expected never appeared and the out-of-scope set measured empty. The 24
in-scope diagnostics were all in the three test files this branch authored,
cleared by annotating declarations to match already-declared nullable returns
plus #nullable enable on those files - the same convention all three
production files use. No NoWarn, no pragma, no severity change, and no
assertion or test name altered.

Tests: 10 added, suite at 6150/6150 across 9 assemblies. Coverage: repo line
85.3890% to 85.4097% and branch 78.5740% to 78.7220%, both improved.
SvgAssemblyProbe reaches 100% line and 100% branch across all three members.
The SvgRenderer class rises 72.1088% to 80.1932% with no line losing coverage.

Acceptance criteria stay at ten of eleven delivered. This cycle added
append-only evidence notes to AC-2, AC-5, and AC-8 and changed no checkbox;
git diff shows zero checkbox lines touched. AC-11 still requires the human
WinForms designer verification.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Feature-review reaudit at head a62391f, verdict PARTIAL with blocking count
changed from 1 to 2:

- policy-audit, code-review, feature-audit, and remediation-inputs for the
  2026-08-04T22-28 cycle.
- remediation-plan.2026-08-05T05-00.md, the cycle-2 plan scoped to the single
  new blocking finding.
- Three feature-review agent-memory entries, including the vstest
  argument-order transitive-dependency finding.

The reaudit verified all of R-2 through R-6 delivered and all seven cycle-1
code-review findings resolved by direct measurement. It then surfaced a new
blocking finding that had been present at the previous head and missed in
cycle 1: SVGControl.Test fails 6 of 75 tests depending on vstest argument
order, because the project references Svg but never ExCSS and legacy
packages.config projects do not flow transitive copy-local. Of nine test
projects it is the only one whose output lacks ExCSS.dll. That violates UT1
Independence.

These artifacts were left uncommitted when the cycle-2 plan was authored, so
that plan's tree-state tasks were written against a clean tree that did not
exist. Committing them restores the premise rather than encoding an exception
for it.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preflight found the plan was authored against a tree state that did not exist:
four reaudit artifacts and three agent-memory files were uncommitted, so the
[P0-T5] halt clause would have fired immediately and [P1-T7]'s remedy would
have directed the executor to revert another agent's memory writes. Committing
the carried-in state as ad60882 restored the premise; this updates the plan's
HEAD references to match rather than encoding a permitted-dirt exception.

- [P0-T5] and the header move to ad60882 with an empty expected porcelain, and
  a non-empty result now permits only halt-and-report - never acting on another
  agent's files. [P1-T7] stays strict; no carried-in set is enumerated.
- Decision 5's reuse argument restated: ad60882 adds only markdown and agent
  memory, no .cs/.csproj/packages.config/app.config, so the 2026-08-05T01-50
  series still describes the current source tree exactly.
- [P1-T2]'s rationale corrected. packages.config is NOT csharpier-exempt -
  .csharpierignore covers *.csproj/*.props/*.targets only, and 26 entries in
  that file are already reflowed. What protects the single-line form is width:
  entries survive to at least 98 characters and the new one is 62.
- [P0-T9] records why UtilitiesSwordfish.Test is excluded from the nine.
- [P2-T5] now dispositions diagnostic removals, not just additions: a
  CoreCompile-gated code that disappears because its project did not recompile
  is not a regression.
- [P1-T4] confirms the HintPath post-build, since Sync-PackageReferences.ps1
  rewrites unresolvable HintPaths and a silent rewrite would invalidate
  [P1-T7]'s added-line count.

Counts unchanged at 11/7/12 = 30, no ID moved.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pinning an expected HEAD SHA was the wrong shape: it encodes a fact about the
world at authoring time into a document that keeps being edited, so each commit
that touched the plan invalidated the plan's own expectation. It rotted once,
was fixed by re-pinning, and rotted again one commit later.

[P0-T5] now records the HEAD SHA as an observation and gates on three
invariants instead:
- git status --porcelain is empty;
- git diff --stat HEAD is empty for both prior plan files, both read-only;
- git diff --name-only a62391f HEAD contains no .cs, .csproj,
  packages.config, or app.config path.

The third is the substantive gate and is strictly stronger than the pin ever
was: a SHA match would have told the executor nothing about whether the source
tree had diverged. It holds across any number of documentation or agent-memory
commits and fails exactly when it should.

Decision 5 restated as a condition rather than a SHA, and [P0-T10]'s reuse
justification swept for the same rot. The only remaining SHA references are the
immutable baseline-capture commit a62391f, used as a diff basis and as the
audited-head fact, and one authored-at observation explicitly labelled as such.

Counts unchanged at 11/7/12 = 30, no ID moved.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
[P0-T9] required the executor to record that UtilitiesSwordfish.Test's project
file is UtilitiesSwordfish.NET.Test.csproj. No such file exists, and no
*Swordfish* project file exists anywhere outside packages/ and .claude/. The
claim came from reading obj/ cache filenames - the .AssemblyReference.cache
entry outlived the tear-down commit - and reporting that inference as a
measurement. An executor following the task verbatim would have asserted a
nonexistent file as verified fact in an audit artifact.

The corrected grounds are strictly stronger: the directory is stale, wholly
untracked build output, evidenced by git ls-files returning zero tracked files,
no matching project file anywhere in the repository, and bin/Debug holding
Swordfish.NET.Test.exe with no *.Test.dll so neither vstest nor the coverage
runner can discover it. The tear-down commit is cited by title rather than SHA.

Also corrected: three measured figures in [P1-T2]'s rationale (19 not 26
multi-line entries, 97 not 98 characters, 63/65 not 62/64); [P2-T1]'s non-zero
branch no longer presupposes a .cs cause, since csharpier also formats
packages.config and app.config; and [P1-T7]'s revert remedy now carries the
same no-act-on-another-agent's-files prohibition [P0-T5] already had, closing
the concurrent-writer route.

Counts unchanged at 11/7/12 = 30, no ID moved.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preflight cleared the cycle-2 plan on pass 3. All three blockers across those
passes were defects in the plan's narrative layer, never in its operative layer:
commands, edits, insert positions, acceptance thresholds, and halt conditions
were correct in every pass, so three passes produced a zero-line change to the
intended diff.

The structural reason this is not cosmetic: several tasks order an artifact to
reproduce their rationale, so a rationale a task requires an artifact to state
becomes evidence in the audit trail, and a reaudit that checks it reopens the
cycle over prose. The false UtilitiesSwordfish project-file ground was one grep
from entering a reference-census artifact as a verified measurement.

Recorded rule: rationale clauses an artifact must reproduce are evidence and
must be measured before they are written, or stated explicitly as inference.
Its specific mechanism gets its own line, because it will recur -
obj/*.csproj.AssemblyReference.cache outlives the project it names, so build
residue read as proof of existence is a repeatable trap; git ls-files <dir> is
the authoritative check.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ependent

SVGControl.Test failed 6 of 75 tests depending on vstest argument order. It
referenced Svg but never ExCSS, and legacy packages.config projects do not flow
transitive copy-local, so of nine test projects it was the only one whose output
lacked ExCSS.dll. The test host probes along the first assembly's path, so the
suite passed when a sibling ran first and failed when it ran alone or first.
That violates UT1 Independence, which requires tests to run in any order.

Before, measured on identical binaries verified by SHA-256:
- standalone: 75 total, 69 passed, 6 failed, all FileNotFoundException for
  ExCSS 4.3.2.0 (innermost request 4.2.3.0)
- SVGControl.Test first, sibling second: 76 total, 70 passed, 6 failed
- sibling first: 76 passed

After:
- standalone: 75 / 75 / 0
- SVGControl.Test first: 76 / 76 / 0

The fix is one <Reference> and one packages.config line, mirroring what the
original plan's P1-T4 already did for Svg for the identical reason. Also adds
<Private>True</Private> to the existing Svg reference, which is behavior-
preserving since MSBuild already defaults HintPath references to copy-local.

Deliberately NOT added: a Fizzler reference, which the cycle inputs directed
"for parity with the eight sibling test projects". That justification is false
on disk - zero test projects reference Fizzler and none carries Fizzler.dll, so
adding it would create divergence rather than parity. Worse, the on-disk
identity is 1.3.1.0 while SVGControl.Test/app.config redirects Fizzler to
1.3.0.0, so adding the assembly would activate a stale redirect that is inert
today only because no Fizzler.dll is present - the same defect class as #418
itself, already filed separately.

Toolchain: analyzer and nullable builds EXIT_CODE 0, zero added diagnostics.
One removal, CS2002 in UtilitiesCS.Test, dispositioned non-regressive because
it is CoreCompile-gated and that project did not recompile. Suite 6150/6150
across 9 assemblies. Repo line 85.4006% and branch 78.6928%, both above floor.
SVGControl package and class coverage figures are byte-identical, because the
nine-assembly ordering already supplied ExCSS - the anticipated improvement did
not materialize and is recorded as such rather than claimed.

AC-11 remains unchecked; it needs the human WinForms designer verification.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Feature-review reaudit at head 69e675d. The ExCSS order-dependence blocker
(G-8) is closed, verified by the reviewer running the discriminating command
itself rather than reading the evidence: SVGControl.Test standalone returns
75/75/0 against 75/69/6 before the fix, and ExCSS.dll is now present in that
project's output. UT1 Independence, UT1 Determinism, UT4, and the C# IDE/CLI
parity rule all return to PASS, and AC-10 is upgraded PARTIAL -> PASS.

The reviewer confirmed the Fizzler refusal was correct and filed it as a
finding against its own cycle-2 remediation inputs rather than against the
branch: every clause of the "parity with the eight sibling test projects"
justification was false, and complying would have activated a stale binding
redirect that is inert only because the file is absent.

One blocking item remains, G-2 / AC-11, the human WinForms designer-load
runbook. The reviewer verified the human_interaction block directly - H-1 and
H-2, both response "exception" with a resolving runbook_path - and deliberately
authored no remediation plan, on the grounds that both open items are
maintainer decisions with no agent-executable task. Its recommendation is to
stop the remediation loop and route to the maintainer; conditional GO, with the
code merge-ready once AC-11 is executed or waived.

It also corrected artifacts/pr_context.summary.txt again, which reported
"Core logic changes: 0 files" against 11 changed C# and build-config files.
That misclassification silently disables C# coverage enforcement, because the
coverage hook derives its changed-language set from that section. Recurring
collector defect, disclosed as G-6.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AC-11 is delivered. The maintainer opened UtilitiesCS/Dialogs/MyBoxViewer.cs in
the Visual Studio WinForms designer at head db8b59f: the form renders
correctly, there is no NullReferenceException, and the default SVG artwork is
visible in the control. All eleven acceptance criteria are now checked.

The maintainer reported no detail in the Output window and asked where to find
it. That is the correct result rather than a gap: SvgRenderer emits its
dual-channel log4net plus Trace diagnostic only on the failure path, and the
parse succeeded, so nothing was written. The ambiguity was resolved by asking
whether the SVG image itself appeared or the area was blank - a blank area would
instead have meant the degrade path fired and a diagnostic should have been
present. The image appeared, so the ExCSS bind resolved inside devenv.exe.

Three limitations are recorded in the capture rather than glossed:

- AC-3's designer-host observability was NOT exercised. Nothing failed, so the
  diagnostic channel was never driven in devenv.exe. The dual-channel behavior
  is proven by unit tests and the degrade-without-throwing behavior by the AC-1
  regression tests, but "an operator would see it in the VS Output window"
  remains verified by construction rather than by observation.
- Attribution of the successful bind is not established. The binding redirect,
  the AC-8 directory-probing fallback, and pre-existing shadow-copy presence
  could each account for it; a pass/fail render cannot separate them.
- Open question U-2 stays open. Runbook step 10's ProjectAssemblies observation
  was not reported. It does not gate AC-11.

G-9 is authorized as an exception by the maintainer. The ratified
COVERAGE_MEMBER_UNREACHABLE exception for ResolveByNameAndKey is extended to the
file-level floor for SvgAssemblyResolver.cs, which that CLR-invoked member now
dominates. Scope is that file only; Install() is unaffected at 6/6. The
orchestrator disclosed before the decision that the new-file threshold applies
only because it sequenced the extraction first to relieve SvgRenderer.cs at
497 of 500 lines, and that the two alternatives - relocating a testable member
to lift the ratio, or reverting the extraction - were rejected.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… waiver

The confirming feature-review returned PASS with blocking count 0 and found four
residual documentation items. Three are fixed here; the fourth needs the
maintainer.

Retract a false statement in the AC-11 capture. It claimed the dual-channel
log4net + Trace behavior is "proven by unit tests in SVGControl.Test". It is
not: that project contains zero occurrences of Trace, log4net, Listener,
Appender, or DescribeFailure, and no test asserts either channel. The
parse-failure tests execute those lines - which is why DescribeFailure measures
100% coverage - but execution is not assertion. The clause was load-bearing,
since it was the fallback offered when disclaiming the observability limitation.
The corrected basis is static inspection of the implementation, which is what
AC-3's operative requirement actually calls for: four paired logger.Error /
Trace.TraceError sites with DescribeFailure composing the exception type and
message. The degrade-without-throwing behavior IS genuinely proven by the AC-1
regression tests.

Correct the U-2 characterization. Recording step 10 as "not reported" read as an
operator omission. It was not: the step is explicitly conditional - "Optionally,
and only if the designer error page reported a failure to load ExCSS" - and no
error page appeared, so the precondition was false and the step was correctly
skipped. The runbook was executed in full.

Transcribe the G-9 maintainer waiver into issue.md under AC-5. It had been
recorded only in artifacts/orchestration/orchestrator-state.json, which is
gitignored, so it would have reached neither the pull request nor a fresh clone,
and the next coverage audit would have re-derived the finding with no record it
was adjudicated. The transcription carries the finding, the authorization, the
basis, the scope, the rejected alternatives, and the disclosure that the
new-file threshold applies only because the extraction was sequenced first.
Verified before writing: no [ExcludeFromCodeCoverage] appears in any changed
source file and none was added anywhere in the diff, so this remains a threshold
exception rather than a measurement exclusion.

Name SVGControl/SvgAssemblyResolver.cs in the coverage-uplift potential entry so
the waived file has an owner instead of resurfacing as an unexplained floor
failure.

No acceptance criterion text or checkbox changed; all eleven remain [x].

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both fields the confirming review flagged as missing are now recorded.

Visual Studio Community 2026, product display version 18.8.2, installation
version 18.8.12023.21, resolved via vswhere rather than asked for again.
Configuration Debug; SVGControl/bin/Debug/SVGControl.dll last built
2026-08-04 23:46:27 UTC, after the final production edit in a62391f.

Visual Studio was NOT restarted after the build - the maintainer rebuilt and
reopened. Recorded rather than smoothed over, because runbook step 2 exists to
guarantee the designer loaded the freshly built assembly instead of one already
resident in the devenv.exe AppDomain. The consequence is specific and is now
stated:

- AC-11 is unaffected. Its criterion is that opening the form loads it without a
  NullReferenceException. The form was opened and it loaded without one. That is
  first-hand observation, not inference.
- The AC-8 corroboration is weakened from "corroborated" to "corroborated
  conditionally". Attributing the successful bind to the directory-probing
  fallback requires that the designer executed the rebuilt assembly. A stale
  in-process assembly could render identically if the ExCSS bind succeeds on
  this host for an unrelated reason - for instance ExCSS.dll already resident in
  the shadow-copy directory, which is exactly what open question U-2 asks and
  which was not measured. So this capture cannot distinguish "the fix worked"
  from "the original failure does not reproduce on this host".

That second point was already recorded under the attribution limitation; the
missing restart is an independent second reason the same attribution cannot be
closed, and both are now recorded so they are not mistaken for one.

What would close it is a five-minute confirmation - restart VS, reopen the form
once, record the result - deliberately not treated as required, because AC-11's
stated criterion does not depend on it.

Refs: #418

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drmoisan
drmoisan merged commit 02ebd26 into main Aug 6, 2026
2 of 4 checks passed
@drmoisan
drmoisan deleted the bug/svg-renderer-null-document-nre-418 branch August 6, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: svg-renderer-null-document-nre

1 participant