fix(UtilitiesCS): treat DirectoryNotFoundException as terminal in WriteTextFileAsync (#707) - #756
Merged
Conversation
Preparation-mode delivery for issue #707: promotion verification, research, spec.md, and an atomic plan cleared through five independent preflight rounds. Narrows the DirectoryNotFoundException retry set in UtilitiesCS/To Depricate/FileIO2.cs (implementation deferred to atomic execution by parallel-orchestrator). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTjXvNFHVh7Fo7kYGgWsx2
Downstream parallel blast-radius harvesting treats any backtick-delimited, whitespace-free, repository-relative path token in spec.md/plan.md as a write target regardless of surrounding polarity. Three defects followed: - scope-exclusion paths (.claude/**, .codex/**, .agents/**, config/blast-radius.json, config/orchestration-routing.json) were quoted in backticks, so the harvester believed this item writes those shared governance surfaces - QuickFiler/Controllers/QfcHomeController.Metrics.cs was backticked in a do-not-touch statement, falsely coupling this item to issue #645, which owns that file - the FileIO2 production path has a space in its directory name (UtilitiesCS/To Depricate/FileIO2.cs), which the harvester could not parse as a single token, so the real production file was missing from the radius entirely This is a semantics-preserving text-presentation revision only: no task, acceptance criterion, command, or evidence path changed. - Add a "## Write Set" section to spec.md naming the two files this plan's diff touches, flagging the space in the FileIO2 directory. - Rewrite every exclusion/do-not-touch/context-reference/verified-no-change path in spec.md and the plan as unbackticked prose, preserving each sentence's meaning exactly. - Re-validated via mcp__drm-copilot__validate_orchestration_artifacts (ok:true) and re-cleared atomic-executor preflight (PREFLIGHT: ALL CLEAR, CONVERGENCE: NO FURTHER ROUNDS EXPECTED). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTjXvNFHVh7Fo7kYGgWsx2
…on-set-707 for reconciliation
…teTextFileAsync (#707) Insert a catch (DirectoryNotFoundException ex) block ahead of the existing catch (IOException ex) in FileIO2.WriteTextFileAsync's internal seam overload, so a missing target directory fails fast (log + return false) instead of consuming the full 100-attempt retry budget. Add a regression test that fails against the pre-fix source (100 factory invocations) and passes post-fix (1 factory invocation, 0 delay invocations). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…AC/plan checkoffs Records the P7-T1/P7-T2 evidence artifacts, the Rollout & Follow-up outcome note in spec.md, and the final plan checklist state for issue #707. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds policy-audit, code-review, and feature-audit artifacts for the #707 bugfix branch. Verdict: PASS, 0 blocking findings across all three artifacts; all 9 spec.md acceptance criteria independently verified. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Suggested title
fix(UtilitiesCS): treat DirectoryNotFoundException as terminal in WriteTextFileAsync
Summary
FileIO2.WriteTextFileAsyncretried everyIOException, includingDirectoryNotFoundException, so a missing target folder consumed the full 100-attempt / 100 ms retry budget even though no retry could ever succeed.catch (DirectoryNotFoundException ex)block immediately before the existingcatch (IOException ex)block: it logs vialogger.Errorand returnsfalseon the first attempt, without incrementing the retry counter or invoking the delay delegate.WriteTextFileAsync_WhenDirectoryDoesNotExist_ShouldReturnFalseWithoutRetrying, confirmed to fail before the fix (100 writer-factory invocations) and pass after (1 invocation, 0 delay invocations).IOExceptioncases (sharing violations, mid-write failures) is unchanged.WriteTextFileAsyncoverload, no caller-side changes.Why
DirectoryNotFoundExceptionderives fromIOException, so the pre-existing singlecatch (IOException ex)block treated it as a transient, retryable condition. A missing directory is a structural failure that cannot be corrected by retrying, so the previous behavior wasted the full retry window (up to 100 attempts at 100 ms delay) before returningfalse. Severity is Low: the one production caller reachable through this path already guards against a missing folder (QuickFiler/Controllers/QfcHomeController.Metrics.cs), but the wasted retry window is still an unnecessary delay whenever the guard is bypassed or the folder is removed mid-run.What Changed
Core fix
UtilitiesCS/To Depricate/FileIO2.cs: insertedcatch (DirectoryNotFoundException ex)ahead of the existingcatch (IOException ex)block in the internal seam overload ofWriteTextFileAsync(pre-change line 126). The new block mirrors the existingopened-terminal-failure shape:logger.Error(...)thenreturn false;, with no call toInterlocked.Increment(ref attempts)ordelayAsync.Tests
UtilitiesCS.Test/HelperClasses/FileIO2_Tests.cs: addedWriteTextFileAsync_WhenDirectoryDoesNotExist_ShouldReturnFalseWithoutRetrying, following the existingwriterFactory/delayseam pattern used by sibling tests. Asserts writer-factory call count1, delay call count0, and resultfalse.Docs/evidence
docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/carriesissue.md,spec.md, the atomic plan, and full baseline/regression-testing/qa-gates evidence for every toolchain and coverage gate, pluspolicy-audit,code-review, andfeature-auditartifacts from an independent review pass.Architecture / How It Fits Together
WriteTextFileAsync's retry loop distinguishes two failure classes inside a singletry: failures before the writer opens (retryable, bounded byattempts >= 100) and failures after the writer opens (opened == true, terminal — a partial write cannot be safely retried).DirectoryNotFoundExceptioncan only be raised by theStreamWriterconstructor (createWriter), i.e. beforeopenedis set, so the new catch block sits alongside the pre-open branch as a second terminal exit, distinguished from the genericIOExceptionbranch purely by exception type. C# requires the more-derived catch clause (DirectoryNotFoundException) to appear before the less-derived one (IOException) in the sametry; this ordering is enforced by the compiler (CS0160), not by convention.Verification
Completed (from evidence in
docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/evidence/):dotnet tool run csharpier check .— EXIT_CODE 0 (clean, no diffs)msbuild TaskMaster.sln /t:Rebuild ... /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true— EXIT_CODE 0, 0 warnings / 0 errorsmsbuild TaskMaster.sln /t:Rebuild ... /p:TreatWarningsAsErrors=true— EXIT_CODE 0, 0 warnings / 0 errors (baseline and post-fix)WriteTextFileAsync_WhenDirectoryDoesNotExist_ShouldReturnFalseWithoutRetryingfailed as expected (observed 100 factory invocations vs. expected 1)FileIO2_Testssuite 12/12 passed, including all 11 pre-existing tests unmodifiedFileIO2.csdelta: 14/14 lines (100%), against the >=90% new-code floorUtilitiesCS.Testsuite: 4769/4786 passed; the 17 failures are pre-existing Deedle/F#dotnet-coverageinstrumentation incompatibilities, identical in the baseline and post-change runs (unrelated to this fix)Recommended (for reviewers who want to reproduce locally):
dotnet tool run csharpier check .msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=truevstest.console.exe UtilitiesCS.Test.dll /InIsolation "/TestCaseFilter:FullyQualifiedName~FileIO2_Tests"Backward Compatibility / Migration Notes
None. No signature change to either
WriteTextFileAsyncoverload. Both production callers (TaskMaster/AppGlobals/AppOlObjects.cs,QuickFiler/Controllers/QfcHomeController.Metrics.cs) already consumeTask<bool>and already handle afalseresult; neither requires a code change.Risks and Mitigations
falseresult will now receivefalseimmediately. Mitigation: both existing callers already treatfalseas a terminal failure signal, so no caller-side logic depends on the delay.scripts/vscode/Invoke-MSTestWithCoverage.ps1) excludes assemblies under a.claudepath segment (tracked separately as issue Bug: coverage-assembly-discovery-excludes-own-worktree-root #752), which this agent's worktree is rooted under. Coverage was captured instead viadotnet-coverage collectwrappingvstest.console.exedirectly, producing equivalent Cobertura output. This is a toolchain substitution with unchanged acceptance conditions, not a reduction in verification.Review Guide
UtilitiesCS/To Depricate/FileIO2.cs— the one-block fix (start here).UtilitiesCS.Test/HelperClasses/FileIO2_Tests.cs— the new regression test, adjacent to the existing sibling test it mirrors.docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/spec.md— Acceptance Criteria (AC1-AC9, all checked) and Proposed Fix section.docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/feature-audit.2026-09-03T08-32.md— independent review verdict.docs/features/active/2026-08-31-narrow-fileio2-retryable-exception-set-707/evidence/— supporting detail only; not required reading for a first pass.Follow-ups
None identified.
PathTooLongExceptionremains explicitly out of scope for this fix perspec.mdScope & Non-Goals.GitHub Auto-close