fix(879): resolve the unsatisfiable netstandard 2.1.0.0 bind that breaks Deedle in the add-in - #896
Merged
drmoisan merged 33 commits intoSep 14, 2026
Conversation
Adds the active feature folder for issue 879 and the research artifact covering the netstandard 2.1.0.0 bind that is unsatisfiable in the production VSTO host. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GSsDVPgS66HpWg9Qroc427
Fills spec.md for the production netstandard bind defect. Records the decided remedy (eager assembly-resolve installer from the ThisAddIn static constructor, resolution logic in a host-neutral UtilitiesCS type), 19 acceptance criteria built on named tests rather than prose searches, and a child-AppDomain harness whose negative control asserts the bind still fails without the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GSsDVPgS66HpWg9Qroc427
Revise the canonical plan in place at the existing plan path. 86 tasks across 7 phases covering the eager AssemblyResolve fallback in UtilitiesCS, its thin ThisAddIn static-constructor call site, the declarative netstandard binding redirect in TaskMaster/app.config, and a child-AppDomain acceptance harness whose negative control is the criterion that distinguishes a fixed build from an unfixed one. Correct one non-canonical evidence directory in spec.md so the coverage artifact lands under a canonical evidence kind. No acceptance criterion was added or removed. The plan records the unexplained netstandard 2.0.0.0 frame as an open risk and states that issue 879 must not be reported as closed on the strength of a 2.1.0.0 result alone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GSsDVPgS66HpWg9Qroc427
Apply all fifteen defects the preflight review returned, plus the two rationale corrections, in place at the canonical plan path. The substantive ones: Phase 0 gains the repo-local SDK install and the packages.config restore without which every later gate failed on first execution; the child-domain harness now pins which domain each assertion runs in, so the no-prior-SVG check can no longer pass vacuously by observing a domain that never loaded the assembly under test; the claim that the installer call is isolated from the negative-control domain becomes a named test rather than prose; two scope gates that were scoped away from every path they prohibit are widened to repository scope; and the targeted test runs drop the coverage collector that instrumented the very process whose assembly set the harness measures. The task count, the phase structure and the acceptance-criteria mapping are unchanged. No acceptance criterion was added or removed and spec.md was not edited in this round. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Apply all nine defects the second preflight review returned. Two were blocking and both were introduced by the first round's own delta: the baseline scope gate demanded an output the fourteen tasks before it make impossible, and the fallback-host contingency named two tasks when nine name the path prefix it substitutes. Also close the vacuity the first round left in place. The handler counter now carries the same positive control the assembly counter gained, so a reflected field that is null in every state can no longer report an empty invocation list unconditionally. That control in turn required the fail-before seam to subscribe its handler while every ladder rung still returns null, which preserves fail-before and is recorded in the task. Correct two stated facts that were false: the repository does carry a Directory.Build.props, and the class-level parallelism attribute narrows the harness exposure rather than removing it. The task count, the phase structure and the acceptance-criteria mapping are unchanged. spec.md was not edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011yGLLemmxqvupFBoajiFUh
Preflight returned ALL CLEAR on the third round against plan blob e04cbb9. The artifact records the round count, the per-round signals, the validator results, the review coverage of the clearing round, and the measured state of the no-prior-SVG guarantee. It also records why three rounds were needed rather than two: two of the nine round-two defects were introduced by the round-one delta itself, and both were blocking. That is recorded rather than excused. Two non-blocking observations the clearing round returned are reproduced verbatim and were deliberately not applied, with the reason stated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011yGLLemmxqvupFBoajiFUh
…d-21-bind-unsatisfiable-in-production-879
Records the analyzer, nullable, format and coverage baselines, the build-output premises, the AppDomain resolve-field probe, the write-set decision (HOST=TaskMaster.Test) and the baseline scope-boundary state. Also records a pre-existing repository-wide analyzer HintPath skew that makes a freshly restored worktree unbuildable, and the git-ignored package bootstrap taken to work around it. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…eria inventory Confirms the planner substituted the canonical evidence/qa-gates/ kind for evidence/coverage/ in spec.md, records the nineteen acceptance criteria by line number with a spot check that all nineteen are still unchecked, records an eight-literal non-canonical evidence path sweep over spec.md that returns zero hits on every literal, and reproduces the authorised write set as a scope lock with Host Substitution HOST=TaskMaster.Test. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
… fail-before harness Adds UtilitiesCS.Bootstrap.AssemblyBindingFallback as a declaration-complete, behaviour-empty seam whose Install subscribes the handler and whose four ladder rungs all return null, ten ladder unit tests in UtilitiesCS.Test, and the child-AppDomain acceptance harness in TaskMaster.Test with its negative control and reflection-shape guards. Each of the three project files gains only Compile Include entries. The targeted rebuild reports zero errors and zero warnings. The expect-fail run records AfterInstall_BothNetstandardVersionsBind as Failed and all five isolation outcomes as Passed; AfterInstall_DeedleTypeInitializerSucceeds unexpectedly Passed, so P2-T11 is left unchecked and reported. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…d-21-bind-unsatisfiable-in-production-879
…ed Resolve signature Revision R2 folds both verified defects into one round. Defect 1: the Deedle probe forced a type initializer and returned OK against a build with no fix, so AC10 could not distinguish a fixed build from a broken one. The probe is repointed at the member invocation Deedle.Reflection.convertRecordSequence, which is the deepest caller frame of the reported production trace, and returns a three-class structured outcome so a netstandard bind failure is distinguished from an unrelated exception. AC10 text is rewritten in spec.md to describe what is now measured, in exactly six lines so no sibling criterion line number shifts. Defect 2: P2-T1 pinned a non-nullable Assembly return on the internal Resolve seam, forcing six null-forgiving returns. The seam becomes Assembly? with the OnAssemblyResolve delegate boundary left as the carved-out suppression. Unchecks and re-runs P2-T1, P2-T5, P2-T6, P2-T10 and P2-T12. Adds P1-T5, P1-T6 and P1-T7 at the end of Phase 1 so no task is renumbered. Corrects the P2-T4 line citation from 193 to 194 after the merge of origin/main a49c972. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…flight defects D1: every vstest span now pins an explicit LogFileName and every TRX reader filters on that name and gates TRX_MATCH_COUNT=1. TestResults/p2-expect-fail already holds a superseded TRX from the version 1.0 run, and Get-ChildItem returns name-ascending, so the unpinned index-zero read would have selected last night's vacuous result and reported a false negative indistinguishable from a real one. D2: the final test gate required zero failures against a tree carrying two recorded baseline failures that are outside this plan's write set, so the gate was unsatisfiable and its restart-the-loop remedy did not terminate. It now gates on the failing set being a subset of those two by name. D3: P4-T7 stated no command at all while its acceptance required outcome lines only a TRX can supply. D4: P6-T24 checked off the live-Outlook acceptance criterion unconditionally, including when the human gate records PENDING-MAINTAINER. It is now conditional with an explicit withheld branch. D5 records the base-commit confound in the coverage delta. D6 corrects a false claim that the probe record mirrors production's shape. D7 records the origin/main anchor substitution in P6-T5 rather than editing acceptance text, to avoid shifting the spec line numbers the check-off tasks cite. D8 adds an eleventh ladder test covering the handler and the production-ladder factory, which no delegate-driven test reaches, and raises the P4-T2 floor to eleven. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…very copy The eleventh ladder test added by R4 drives the real CLR binder, which falsifies a claim that exists in three places. R3 scoped only the plan's copy. R4 closes the other two: P2-T3 now rewrites the class-level XML doc comment in the test file and gates on a zero-hit search for the stale fragment, and P6-T9 records the reading under which AC4 remains satisfied, following the same record-in-the-consuming-task pattern the plan already uses for AC14 and AC18. The acceptance-criteria status summary names the qualification. The spec is deliberately not amended. AC4 spans seven lines and a re-wrap would move the fifteen criteria after it, every one cited by line number by a Phase 6 check-off task. Also corrects the plan header to revision R4, and adds a working-directory guard: an executor launched without worktree isolation inherits a different checkout, in which case every repository-relative path in this plan resolves into the wrong tree silently. A further copy of the same failure mode was found and fixed while applying these deltas: the sentence asserting no test file changes, alongside eight exact line citations that the eleventh test shifts. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
P1-T5 preserves the superseded P2-T11 artifact recording the vacuous AfterInstall_DeedleTypeInitializerSucceeds pass, so the measurement that identified Defect 1 survives the re-run that overwrites the plan-named path. P1-T6 reads the Deedle member surface from metadata only: convertRecordSequence is Assembly-accessible and static with exactly one generic parameter and one definition, so P2-T5 can be authored against it and its NonPublic binding flag is load-bearing. P1-T7 confirms the AC10 rewrite occupied exactly six lines and displaced no sibling criterion. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…d fail-before run P2-T1 makes the internal Resolve seam nullable and drops the three null-forgiving returns on its dominant path, leaving the three inside OnAssemblyResolve as the declared ResolveEventHandler boundary suppression. P2-T3 adds an eleventh ladder test that drives the subscribed handler through the real CLR binder, which is the only path that reaches OnAssemblyResolve and CreateProductionLadder, and rescopes the class comment that claimed no test reaches a real bind. P2-T5 repoints the probe at an invocation of Deedle.Reflection.convertRecordSequence closed over a record type and adds a three-class structured outcome. P2-T6 repoints the one test body and writes the observed value to the TRX. P2-T10 rebuilds clean, 0 errors and 0 warnings. P2-T12 records all five isolation outcomes as Passed, so the harness has isolation. P2-T11 is left UNCHECKED and the executor halts. The repointed probe recorded DEEDLE_RECORD_CONVERSION_OUTCOME=INVOKED-NO-EXCEPTION and the test passed, against a build carrying no fix. That is the same defect class revision R2 was written to close, on a different member. Isolation is intact and the bind is genuinely unsatisfiable in the same child domain, where AfterInstall_BothNetstandardVersionsBind failed with FileNotFoundException on the 2.1.0.0 identity, so the reading is an observation rather than an artefact. The probe was not adapted and the acceptance condition was not weakened. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
… entry at the quota wall
… while investigating 879 The six FSharp.Core HintPath entries are split between the netstandard2.0 and netstandard2.1 flavours of FSharp.Core.11.0.100. The netstandard2.1 copy references netstandard 2.1.0.0, an identity that does not exist for .NET Framework on any machine, so it is unloadable wherever it is deployed. Which flavour lands in a given output directory is last-writer-wins build-order nondeterminism, so a project can flip between working and broken across a rebuild with no source change. Found while investigating 879. Two successive regression-probe designs for that issue returned green against an unfixed tree solely because the probe child domain was rooted at a directory where nothing requests netstandard 2.1.0.0. The probe designs were correct; the directory was wrong. Out of scope for 879 and tracked separately: the three project files carrying the netstandard2.1 HintPath are outside that item's authorised write set. The 879 remedy remains correct and complementary, since it makes the add-in robust to whichever flavour is deployed. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…y that reproduces the defect Measurement found the discriminator. It is the child domain ApplicationBase, not the Deedle call shape. FSharp.Core.11.0.100 ships two binaries and the repo HintPaths are split; QuickFiler, QuickFiler.Test and ToDoModel take the netstandard2.1 flavour, whose netstandard 2.1.0.0 reference nothing on a .NET Framework machine satisfies. TaskMaster.Test output received the netstandard2.0 flavour, so nothing there ever requests 2.1.0.0 and every probe rooted there succeeded regardless of the fix. All three probe designs were correct, including the original type-initializer force; all three were pointed at the wrong directory. Every child domain now roots at the QuickFiler.Test build output, because a split root would break the harness design argument: the isolation observations must describe the same domain the bind result comes from. AC10 is rewritten to name the ApplicationBase as the load-bearing element, in six lines so no acceptance-criterion line number moves. Re-rooting exposed a latent break the brief did not anticipate. The harness created its probe by display name, which resolves against the child ApplicationBase, and the host test assembly is absent from that directory. The task now specifies the location-based form with a zero-hit gate on the display-name form. Adds two Phase 5 tasks replacing six raw console logs with redacted projections before the first committing task, closing the projections-only policy gap. Root cause of the HintPath skew is out of scope here and tracked as issue 895. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…f this run The results directory already held a file bearing the pinned logger name, written by the superseded revision R2 re-run and carrying the vacuous outcome this revision exists to eliminate. The match-count gate therefore read one whether the run emitted a result or emitted nothing, so a run that failed to start would have silently replayed the earlier vacuous values. Each affected task now removes stale result files before the run and records a pre-run count of zero, which converts the match count from a file exists into this run wrote it. Applied to the fail-before task and to the three Phase 4 test spans. The fourth reader is deliberately excluded: it reads a directory an earlier task in the same phase writes, so emptying it there would delete the result the reader must consume. Also corrects two stale discriminating readings in an already-executed task whose own execution invalidated them, and labels them as standing guards rather than as evidence that the task ran. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
P1-T6 re-measures the Deedle member surface from QuickFiler.Test/bin/Debug/Deedle.dll and records the Deedle.Frame.FromRecords overload set. P1-T7 confirms the R5 AC10 rewrite displaced no sibling criterion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…before P2-T5 repoints the probe to Deedle.Frame.FromRecords with a shape-filtered lookup and adds ApplicationBaseDirectory. P2-T6 roots every child domain at the QuickFiler.Test build output, creates the probe from its assembly location, and adds the ninth test asserting the root. P2-T11 now records NETSTANDARD-BIND-FAILURE:TypeInitializationException against an unfixed build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…-module coverage floor All four CLAUDE.md steps passed in a single pass with no restart: csharpier format then check at the fixpoint, analyzers with zero warnings and zero errors, nullable with treat-warnings-as-errors at zero, and the full suite at 7283 of 7283 passed with zero failures. The format pass rewrote two files, both inside the authorised write set, and reduced the tightest production file rather than growing it, so the 500-line ceiling risk is closed with 34 and 45 lines of headroom. Blocked at the new-module coverage measurement. The module reports 166 of 201 lines covered, 82.59 percent, against the 90 percent floor stated by the acceptance criterion itself. Four confounders were ruled out: aggregation is complete at one class row carrying all 201 lines, every driving test ran and passed, the document is post-processed, and the figure is a line percentage rather than a branch or rolled-up rate. The uncovered lines are guard clauses, rung-local catch blocks, and the fourth ladder rung, which has no delegate-driven test. Closing the gap needs new test methods, which conflicts with two standing guards pinning the test count, so it is a plan revision rather than an executor action. No gate was weakened and no denominator reselected. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
…g test class The module measured 166 of 201 lines covered, 82.59 percent, against the 90 percent floor its own acceptance criterion states. The uncovered lines are guard clauses, rung-local catch blocks, and the fourth ladder rung, which had no delegate-driven test. Ten new tests are added in a new sibling class rather than by amending the existing one. That keeps the existing class byte-unchanged, so both of its guards stay true and unmeasured, and it leaves the criterion that names that class outside the change entirely. Projected coverage is 189 of 201, 94.03 percent, with eight lines of margin and an unmoved denominator. Three lines suggested for coverage were rejected as unreachable, and nine more were excluded with reasons recorded rather than quietly dropped, because their only throw sources were never observed and the contract forbids asserting on unobserved output. The fail-before evidence is untouched. The two tasks that produced it stay checked, because re-running them against a tree that now carries the fix would record a pass and destroy the only fail-before measurement this work has. The new tests live in a different assembly that neither task compiles or runs. Phase 5 is unchecked in full: a source change invalidates a completed toolchain loop, so it restarts from formatting. Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com Claude-Session: https://claude.ai/code/session_012YZxqEe1udErQiYYt6Bb2d
The final task marks itself complete, so its own check-off cannot be included in the commit it describes. This commit records that mark and leaves the worktree clean.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(879): resolve the unsatisfiable netstandard 2.1.0.0 bind that breaks Deedle in the add-in
Summary
UtilitiesCS.Bootstrap.AssemblyBindingFallback, a host-neutral assembly-resolution ladder that satisfies anetstandard 2.1.0.0request with the2.0.0.0facade the machine actually has, and installs it eagerly from theThisAddInstatic constructor so it is in place before any ribbon handler runs.netstandardbinding redirect toTaskMaster/app.configas defence in depth, so the add-in is hardened whether or not the managed fallback is reached first.AppDomainregression harness that reproduces the reported production failure and a fail-before/pass-after pair proving the remedy, rather than asserting it.Why
The maintainer reported that clicking the QuickFiler ribbon button in a fresh Outlook session raised a
TypeInitializationExceptionfromDeedle.Reflection, chaining through<StartupCode$Deedle>.$FrameUtilsto aFileNotFoundExceptionfornetstandard, Version=2.1.0.0. The QuickFiler data model did not populate.netstandard 2.1.0.0does not exist for .NET Framework on any machine, so that request can never be satisfied by probing. The request does not originate in Deedle:Deedle.dll3.0.0.0 referencesnetstandard 2.0.0.0. It originates inFSharp.Core.The
FSharp.Core.11.0.100package ships two binaries,lib/netstandard2.0andlib/netstandard2.1, and this solution'sHintPathentries are split between them. Thenetstandard2.1binary referencesnetstandard 2.1.0.0and is therefore unloadable wherever it is deployed. Which binary lands in a given output directory is last-writer-wins build ordering, so a project can move between working and broken across a rebuild with no source change.This PR does not change those
HintPathentries. That work is tracked separately, and the two remedies are complementary: the separate issue removes the unloadable binary, while this PR makes the add-in tolerate whichever binary is present.What Changed
Core fix
UtilitiesCS/Bootstrap/AssemblyBindingFallback.cs— new. A four-rung resolution ladder behind five injected delegates, with a[ThreadStatic]re-entrance guard, anInterlocked.Exchangeidempotence guard onInstall(), and a boundarycatchso no exception reaches the CLR binder. References no WinForms type, no Outlook Interop type, and no logging framework.TaskMaster/ThisAddIn.cs— adds an explicit static constructor whose body is a singleAssemblyBindingFallback.Install()call. The eager installation point matters: the failure occurs on the first ribbon click, before any lazier hook would run.TaskMaster/app.config— adds anetstandarddependentAssemblyredirecting0.0.0.0-2.1.0.0to2.0.0.0.Tests
UtilitiesCS.Test/Bootstrap/AssemblyBindingFallbackTests.cs— 11 tests driving every rung through injected delegates.UtilitiesCS.Test/Bootstrap/AssemblyBindingFallbackEdgeCaseTests.cs— 10 tests covering the guard clauses, the rung-localcatchblocks, and the probe-directory rung.TaskMaster.Test/Bootstrap/ChildDomainBindProbe.csandNetstandardBindChildDomainTests.cs— aMarshalByRefObjectprobe and a nine-test harness that creates childAppDomains, performs each assertion inside the domain, and marshals primitives back.TaskMaster.Test/Bootstrap/AddInEagerInstallShapeTests.cs— guards the eager install point and the config redirect against silent deletion, by reflection and XML assertion rather than by prose search.Project files
Four
Compile Includeregistrations only. These projects use explicit compile items with no wildcard glob, so an unregistered file silently does not build. NoHintPath, reference, or property was changed.Architecture / How It Fits Together
ThisAddIn's static constructor callsAssemblyBindingFallback.Install(), which subscribes a handler toAppDomain.CurrentDomain.AssemblyResolveexactly once. When the CLR fails to bind a name, the handler consults a ladder that tries, in order: an already-loaded assembly whose identity matches; a load by full display name; a load from the runtime directory; and a load from the add-in's own probe directory. Each rung absorbs its own failures and declines, so a miss isnullrather than a thrown exception, which is what theResolveEventHandlercontract requires.Every external dependency is an injected delegate, so the ordering logic is unit-testable without touching the GAC or the filesystem. The production wiring is supplied by a single factory.
Verification
Completed
EnableNETAnalyzersandEnforceCodeStyleInBuildat 0 warnings and 0 errors; nullable analysis withTreatWarningsAsErrorsat 0 warnings and 0 errors; and the full suite at 7293 passed, 0 failed.CoreCompilewas skipped on zero projects, so the analyzers actually ran.DEEDLE_RECORD_CONVERSION_OUTCOME=NETSTANDARD-BIND-FAILURE:TypeInitializationException.INVOKED-NO-EXCEPTION.ApplicationBaseis verified by its own passing assertion, and the negative control — thatnetstandard 2.1.0.0still throws in an installer-free domain — passed in both runs, so the positive result is attributable to the fix rather than to a loss of isolation.netstandard.dllentered any project or output directory, and theFSharp.Coreredirect count is unchanged from baseline.Recommended
Backward Compatibility / Migration Notes
No public API is removed or renamed. All source changes are additive except the eager install point, the config redirect, and four compile-item registrations. The resolution handler only participates when the CLR has already failed to bind a name, so it cannot change the outcome of a binding that previously succeeded.
Risks and Mitigations
2.0.0.0frame. This PR does not explain it. A measurement taken during this work shows that in an installer-free child domain on this machine,netstandard 2.0.0.0binds successfully while2.1.0.0throws, which localises the open question to the add-inAppDomainrather than the machine. This issue should not be considered closed on a2.1.0.0result alone.FSharp.Corebinary, which is build-order luck rather than correctness. It can move back to the unloadable one on any rebuild. That is the separately tracked root cause; this PR's remedy does not depend on which binary is present.Review Guide
Suggested order:
UtilitiesCS/Bootstrap/AssemblyBindingFallback.cs— the whole fix is here.TaskMaster/ThisAddIn.csandTaskMaster/app.config— the two wiring points.TaskMaster.Test/Bootstrap/NetstandardBindChildDomainTests.cs— the harness, including the negative control and the isolation assertions that make its positive results meaningful.UtilitiesCS.Testclasses — mechanical, delegate-driven, and safe to skim.The diff is 2096 insertions and 0 deletions across 11 source files, so there is no mechanical move or rename to work around.
Follow-ups
FSharp.CoreHintPathentries on the loadable binary, and consider a build-time guard asserting that no deployedFSharp.Corereferencesnetstandard 2.1.0.0. Tracked separately.2.0.0.0question in the production trace.GitHub Auto-close
Acceptance criteria: merging at 18 of 19, with AC19 withheld
This change merges with 18 of 19 acceptance criteria checked. AC19 is not checked, and the
check-off is withheld pending the maintainer.
AC19 requires a live Outlook session. No Outlook session was started, and nothing about the
criterion was simulated, approximated, or inferred from a proxy. Task
[P6-T24]therefore withheldthe check-off rather than discharging it, and
spec.mdline 554 remains- [ ]with an explicitCheck-Off Withheld:record beside it. The criterion is undischarged, not satisfied.This follows the pattern already established in this run for a criterion that cannot be discharged
by an automated agent: merge the verified work, leave the box unchecked, state the gap plainly in
the body, and leave the discharge to the maintainer.
A correction to this change's own stated diagnosis
This item was halted and replanned on the premise that AC10 was a vacuous acceptance
criterion — one that passed against a build containing no fix. That premise was wrong, and it is
recorded here rather than quietly dropped.
AC10 was never vacuous. The probe was rooted in a directory that could not reproduce the
defect.
FSharp.Core.11.0.100ships two binaries and this solution'sHintPathentries are splitbetween them;
TaskMaster.Test/bin/Debugreceived the loadable flavour, so nothing rooted thereever requests
netstandard 2.1.0.0and every probe rooted there passes regardless of the fix.Rooted correctly, all three probe designs reproduce the failure, including the original design
that was dismissed as vacuous.
The requirement that drove this out was the demand for a demonstrated pre-fix failure rather than an
argued one. That requirement was invoked for a reason that turned out to be incorrect, and it
surfaced the true cause anyway. The general lesson stands independently of this item: a criterion
that has never been observed failing is unproven, and demanding the failure be shown is cheaper than
arguing about whether it would occur.
The underlying
HintPathsplit is filed separately as issue #895 with the measured evidence. It isout of scope here because the affected project files may be owned by sibling items in the same
parallel run.