From 10adbc03eab3906c9409143abe9d54eb5d296687 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sat, 29 Aug 2026 18:02:04 -0400 Subject: [PATCH 01/16] wip(637): checkpoint preparation artifacts before session pause Preparation for issue 637 (breadcrumb SelectRow rooted-path/D1 half-close) was interrupted mid-run: issue, spec, plan, and research are written but preflight clearance had not started. Commits agent-memory learnings from this preparation pass and one promoted latent-defect record surfaced during research. Co-Authored-By: Claude Sonnet 5 --- .claude/agent-memory/atomic-planner/MEMORY.md | 2 + ...it-status-may-describe-another-worktree.md | 30 + ...ct_637_selectrow_rooted_path_plan_seams.md | 55 + .claude/agent-memory/orchestrator/MEMORY.md | 1 + ...tes-harness-gitstatus-of-wrong-checkout.md | 49 + .../agent-memory/task-researcher/MEMORY.md | 1 + .../project_selectrow_two_families_637.md | 22 + .../issue.md | 143 ++ .../plan.2026-08-29T12-20.md | 1338 +++++++++++++++++ .../research/research.2026-08-29T12-30.md | 753 ++++++++++ .../spec.md | 1054 +++++++++++++ ...board-paths-and-half-completed-teardown.md | 135 ++ 12 files changed, 3583 insertions(+) create mode 100644 .claude/agent-memory/atomic-planner/harness-git-status-may-describe-another-worktree.md create mode 100644 .claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md create mode 100644 .claude/agent-memory/orchestrator/subagent-cites-harness-gitstatus-of-wrong-checkout.md create mode 100644 .claude/agent-memory/task-researcher/project_selectrow_two_families_637.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/issue.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/research/research.2026-08-29T12-30.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md create mode 100644 docs/features/potential/promoted/2026-08-29-efc-ok-chain-unhandled-keyboard-paths-and-half-completed-teardown.md diff --git a/.claude/agent-memory/atomic-planner/MEMORY.md b/.claude/agent-memory/atomic-planner/MEMORY.md index 403342c3b..96d3a8916 100644 --- a/.claude/agent-memory/atomic-planner/MEMORY.md +++ b/.claude/agent-memory/atomic-planner/MEMORY.md @@ -19,6 +19,7 @@ - [#677 R1–R8](project_677_keyboard_focus_leak_plan_seams.md) — ctor param REJECTED (5 reflection-arity tests); typed harness for compile-red; internal 9-arg ctor, never ambient SetSynchronizationContext; per-file non-vacuity floors - [#635](project_635_reflective_caller_audit_plan_seams.md) — evidence-only audit: tracked plan inflates its own sweep; scan hits its own pattern list; spec said six sites, tree has eight; pathspec breadth inflates a count - [#440 R1–R4](project_440_breadcrumb_left_arrow_plan_seams.md) — deletion-only change voids a diff-derived changed-line gate; `(Rebuild target(s))`, NOT `(Rebuild target)`; `.csharpierignore` matches the `.cobertura.xml` suffix; `.dotnet-sdk` IS gitignored (`.gitignore:350`); cite an AC by sentence only after counting its sentences; `Include` resolves against the declaring project's dir +- [#637 R2/R3](project_637_selectrow_rooted_path_plan_seams.md) — `docs/features/active` operand hits 121 sibling evidence files; the untracked-440 claim was FALSE for the agent worktree; blanket `-F` breaks every regex; pre-format range consumed post-format; probe with no branch for the outcome that occurs - [#680](project_680_menu_mode_plan_seams.md) — HostTests.cs 499 not 500; set-difference format gate; TRX 5-shape identifiers, `grep -a`; append-a-dated-literal discriminator; post-merge remediation: exact line arithmetic — the review's "optional" fallback was load-bearing (501 vs 500) ## Plan-structure traps @@ -48,6 +49,7 @@ - [Enumeration variable must match its consumer](enumeration-variable-must-match-consumer.md) — `$kept` produced vs `@assemblies` splatted = zero-assembly vstest run reporting zero failures - [Diff gates need a commit task](diff-gates-need-a-commit-task.md) — `git diff ..HEAD` passes vacuously with no commit task - [Never pin a HEAD SHA as a plan expectation](never-pin-head-sha-as-plan-expectation.md) — gate on tree invariants instead +- [Harness gitStatus may describe another worktree](harness-git-status-may-describe-another-worktree.md) — measure inside the target worktree (index grep + negative control) or mark the fact unverified - [Absolute counts in shared files go stale](absolute-counts-in-shared-files-go-stale.md) — lower-bound/baseline-relative for co-owned files; keep exact the count the task changes - [.claude/agent-memory is tracked](agent-memory-is-tracked-scope-git-gates.md) — scope every diff/status/grep gate or it is unsatisfiable - [Stale build output is not evidence of existence](stale-build-output-is-not-evidence-of-existence.md) — verify with `git ls-files`, not `obj/` diff --git a/.claude/agent-memory/atomic-planner/harness-git-status-may-describe-another-worktree.md b/.claude/agent-memory/atomic-planner/harness-git-status-may-describe-another-worktree.md new file mode 100644 index 000000000..2fdcf6a8d --- /dev/null +++ b/.claude/agent-memory/atomic-planner/harness-git-status-may-describe-another-worktree.md @@ -0,0 +1,30 @@ +--- +name: harness-git-status-may-describe-another-worktree +description: The gitStatus block supplied at session start can describe a different checkout than the agent worktree a plan targets; never quote it as a fact about the target worktree. +metadata: + type: feedback +--- + +Never cite the harness-supplied `gitStatus` block as an environment fact about the worktree a plan +targets. Measure inside the target path, or mark the fact unverified. + +**Why:** on issue #637 the harness status described the session worktree +`TaskMaster-wt/2026-08-29T00-11`, where `docs/features/active/2026-08-07-...-440` was untracked. The +plan targeted the agent worktree `.claude/worktrees/agent-a68051a23e4479267`, where the same folder is +tracked — it had merged to `main` at the branch base commit. A preflight reviewer's contrary judgment +("every sibling folder is committed and clean") was correct and was overruled on the strength of the +wrong checkout's status. This repository routinely has several worktrees checked out at once, so the +two states diverge as a matter of course rather than exceptionally. + +**How to apply:** before writing any tracked/untracked, dirty/clean, branch or HEAD claim into a plan, +run the command with `git -C ` and observe the output. When no shell tool is +available in the session, two substitutes are workable and both were used on #637 R3: +- tracked-ness: grep the target worktree's index for the path literal. Resolve the index via the + worktree's `.git` file (`gitdir:
/.git/worktrees/`); the index is binary but ripgrep + reports a match. Pair every positive probe with a negative control — a path known to be untracked — + so a spurious match is detectable. Index membership proves tracked, not unmodified. +- presence on disk: `Glob`/`Grep` over the path. +Otherwise state the fact as unverified rather than inferring it. Prefer a justification that does not +depend on observed tree state at all: an executor runs later than the planning pass, so a +forward-looking reason ("a concurrent run can create this condition before execution") is both true +and stable. See [[project_637_selectrow_rooted_path_plan_seams]]. diff --git a/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md b/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md new file mode 100644 index 000000000..f33d3f088 --- /dev/null +++ b/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md @@ -0,0 +1,55 @@ +--- +name: project-637-selectrow-rooted-path-plan-seams +description: Issue #637 breadcrumb SelectRow plan — preflight R2/R3 seams: docs/features/active as a command operand reaches 121 sibling evidence files; the untracked-440 claim was false for the agent worktree; a blanket -F rule breaks every regex; pre-format line ranges consumed post-format. +metadata: + type: project +--- + +Preflight round-2 seams from the issue #637 atomic plan (breadcrumb `SelectRow` emits a rooted path). + +**Never use `docs/features/active` as a command operand.** A token scan with that directory operand +returned matches from **121 distinct evidence Markdown files** belonging to other feature folders +(re-measured 2026-08-29 in the agent worktree: still exactly 121, none under this feature's folder), +so a zero-hit gate over it can never pass. That reason is git-tracking-independent — `rg` reads the +working tree — and it is the only sibling-contribution verdict that survives round 3. + +**CORRECTED (R3): the untracked-440 claim was false for the agent worktree.** Round 2 asserted +`docs/features/active/2026-08-07-...-440` was "present and untracked" and made that the justification +for narrowing every git pathspec. In the agent worktree +`.claude/worktrees/agent-a68051a23e4479267` the 440 folder is **tracked**: `.../440/spec.md` and +`.../440/evidence/baseline/phase0-instructions-read.md` are both present in that worktree's git index, +while this feature's own `.../637/spec.md` is absent from it. The claim came from a harness-supplied +git status describing the *session* worktree `TaskMaster-wt/2026-08-29T00-11`, where 440 genuinely was +untracked. See [[harness-git-status-may-describe-another-worktree]]. +Keep the narrowing; state it forward-looking: a parent-directory `git add` would stage any untracked +sibling folder that a concurrent run creates between planning and execution, and the plan cannot +assume the tree it observed is the tree the executor meets. +**Why:** the parent directory is shared with every other in-flight feature, none of which this plan owns. +**How to apply:** scope every `rg` directory operand, `git add`, `git status --porcelain` and `git diff` +pathspec to the feature's own folder spelled in full. A FEATURE_DIR convention that says "this path is +spelled once" must be amended — commands cannot carry a placeholder. + +**A pre-format line range consumed by a post-format assertion is unsound.** The plan recorded the new +helper's line range in Phase 4, ran the write-mode formatter in Phase 7, then evaluated coverage +against the Phase-4 range. Fix: re-derive the range in the consuming task against the post-format +tree, record both ranges, state whether they differ, and evaluate every assertion against the +re-derived one. Sweep every line number/hunk range captured before the first format pass. + +**A convention sentence that generalizes a flag is a defect generator.** "Where the pattern contains a +backslash, `-F` is used" attached `-F` to every regex in the plan, because most backslashes were +regex escapes (`\s`, `\(`, `\[`) — `-F` matches them literally and returns zero matches. Correct form: +name the two sites that genuinely need `-F` and state that every other pattern is a regex issued +without it. See also [[zero-hit-grep-gates-need-carveouts]]. + +**Exact-count gates versus the same task's authoring instruction.** Beyond the known `MoveToFolder` +case, three more surfaced in one file: a replacement comment naming `IsFullOutlookPath` (count 1), +XML documentation phrasing AC12's contract (`IsFullOutlookPath` again), and a doc sentence saying +"never throws" against an `rg -n "throw"` before/after set-equality gate. Also `out string stem` +(count 2), `out _` (count 0), and `Globals.Ol.ArchiveRootPath` (count 4). Sweep every asserted token +against every authoring instruction in the same *and* later tasks over the same file. +See also [[single-numeral-gates-must-name-the-role]] and [[absolute-counts-in-shared-files-go-stale]]. + +**A probe must state what each outcome means.** An `ON_PATH`/`NOT_ON_PATH` `msbuild` probe was added +to justify a vswhere substitution, but the plan supplied no branch for `ON_PATH` — which is the +outcome that actually occurs in this worktree — so the artifact would have recorded an observation +refuting the justification it supports. Write both branches. diff --git a/.claude/agent-memory/orchestrator/MEMORY.md b/.claude/agent-memory/orchestrator/MEMORY.md index e3ccdd636..6354bb177 100644 --- a/.claude/agent-memory/orchestrator/MEMORY.md +++ b/.claude/agent-memory/orchestrator/MEMORY.md @@ -92,6 +92,7 @@ - [Removing a halt requires branch propagation](removing-a-halt-requires-branch-propagation.md) — converting a HALT into a recorded blocker - [Prepared epic child invalidated by a sibling merge](prepared-epic-child-invalidated-by-sibling-merge.md) — a merged fix for an issue the - [Bash tool rejects complex commands in isolated worktrees](bash-tool-rejects-complex-commands-in-isolated-worktree.md) +- [Subagent cites harness gitStatus of the wrong checkout](subagent-cites-harness-gitstatus-of-wrong-checkout.md) — an untracked-file claim may describe the session worktree, not yours; re-measure before it lands in a plan - [#457 coverage moved UP, and the kickoff figure was wrong](project_457_coverage_moved_up_not_down.md) — the denominator fix raised the - [atomic-planner has no MCP validator tool](atomic-planner-lacks-mcp-validator-tool.md) — it cannot run the mandatory plan gate; run it - [Preflight defect-trend scope confound](preflight-defect-trend-scope-confound.md) — a rising count isn't divergence if you widened scope diff --git a/.claude/agent-memory/orchestrator/subagent-cites-harness-gitstatus-of-wrong-checkout.md b/.claude/agent-memory/orchestrator/subagent-cites-harness-gitstatus-of-wrong-checkout.md new file mode 100644 index 000000000..0a42eb704 --- /dev/null +++ b/.claude/agent-memory/orchestrator/subagent-cites-harness-gitstatus-of-wrong-checkout.md @@ -0,0 +1,49 @@ +--- +name: subagent-cites-harness-gitstatus-of-wrong-checkout +description: A subagent can cite the harness-supplied git status block as an observation about YOUR worktree when it actually describes the session checkout; verify any untracked/dirty claim yourself before it lands in a plan +metadata: + type: feedback +--- + +A delegated agent running against an isolated agent worktree may cite the **harness-supplied +`gitStatus` context block** as if it were a measurement of that worktree. That block describes the +**session checkout**, which in this repository is routinely a different path with different +untracked files. + +**Observed 2026-08-29 (issue #637 preparation, parallel run bugs-638-644-647).** `atomic-planner` +rejected a preflight reviewer's premise, asserting that +`docs/features/active/2026-08-07-breadcrumb-left-right-arrow-parent-child-navigation-440` was +"present and untracked in this worktree" and writing that observation into the plan as the stated +justification for narrowing seven `git add` / `git status --porcelain` pathspecs. Measured directly +in the agent worktree, `git status --porcelain -- docs/features/active` listed only the feature's own +folder: the 440 folder was **tracked**, having merged into `main` at the branch's own base commit +`ecdb1c84` (subject: "Merge pull request #689 from drmoisan:bug/...-440"). The claim was true only of +the session worktree `TaskMaster-wt/2026-08-29T00-11`, whose start-of-session status block listed 440 +as untracked. + +**Why this is worth catching rather than tolerating:** the *action* was correct — narrowing a broad +`docs/features/active` pathspec is right defensively, because the executor runs later than the +planning pass and a concurrent sibling can introduce an untracked folder in between (see +[[feedback_git_add_a_sweeps_unrelated_queued_promotions]] in the user-scope memory for the real +incident this guards against). Only the *justification* was false. A correct fix resting on a false +citation is the worst shape to ship: it survives review because the fix looks right, and the false +fact then propagates into every later document that cites the plan. It also inverted the record — +the reviewer's original premise was accurate and was overridden. + +**How to apply:** +- Before accepting any subagent claim about tracked/untracked/dirty state, branch topology, or file + presence, re-measure it yourself with `git -C ...`. Cheap, and it is the + only authoritative source. +- When a subagent *rejects* a reviewer premise on environment grounds, treat that as a high-value + verification trigger rather than as evidence of diligence. Two premise rejections in the same run + were correct and upheld (`Helpers.ps1` line citation, `branch="True"` capitalization); this third + was not, and they read identically in the report. +- Arm delegation prompts for isolated worktrees with: name the authoritative worktree path, and state + that any environment fact not measured by a command run inside that path is unverified and must be + marked as such rather than inferred from a supplied status block. +- Keep the defensive fix when the action is right and the reason is wrong; correct the reason in place + rather than reverting the edit. + +Related: [[agent-worktree-hooks-resolve-to-agent-cwd]] and +[[collect-pr-context-lands-in-main-checkout]] — both are the same underlying hazard, a tool or agent +resolving "here" to a checkout other than the one the caller means. diff --git a/.claude/agent-memory/task-researcher/MEMORY.md b/.claude/agent-memory/task-researcher/MEMORY.md index d5ba91192..a86b4d3c3 100644 --- a/.claude/agent-memory/task-researcher/MEMORY.md +++ b/.claude/agent-memory/task-researcher/MEMORY.md @@ -45,6 +45,7 @@ - [qfc677-webview2-focus-hold-outlook-keyboard](project_qfc677_webview2_focus_hold_outlook_keyboard.md) — #677: Outlook keyboard death = WebView2 focus hold (WV2Feedback #951) + unconditional FinishClose _focusAnchor steal, NOT a repo hook; fix = focus-permission predicate + deactivate parking (2026-08-28) - [qfc680-menu-mode-keyboard-capture](project_qfc680_menu_mode_keyboard_capture.md) — #680: ModalMenuFilter retargets keys on non-focusing ToolStripDropDown show; AutoClose=false pre-Show is the only public opt-out; #438 HV-1 residual, not fixable via #677 MayTakeFocus (2026-08-28) - [reflective-caller-closure-635](project_reflective_caller_closure_635.md) — #635/#468: removal was THIRTEEN members (`_templateTlp` omitted from AC-16); `GetField(` never searched (172 hits) yet is the only reaching mechanism; QuickFiler is ComVisible(false) (2026-08-29) +- [selectrow-two-families-637](project_selectrow_two_families_637.md) — #637: TWO unrelated SelectRow families (bare grep over-counts ~10x); blanket TryMakeArchiveRelative rejects relative/Trash rows; ButtonOK_Click does NOT rethrow (2026-08-29) ## Artifact hygiene - [Never embed absolute host paths](../_shared_no_absolute_host_paths.md) — no `C:\Users\\...`, bare account, or machine name in ANY artifact; use `` / `` / `` / ``. vstest names TRX `__.trx` by default, so control `/ResultsDirectory:` + `LogFileName=` or rename before citing. diff --git a/.claude/agent-memory/task-researcher/project_selectrow_two_families_637.md b/.claude/agent-memory/task-researcher/project_selectrow_two_families_637.md new file mode 100644 index 000000000..6d4c5e5d1 --- /dev/null +++ b/.claude/agent-memory/task-researcher/project_selectrow_two_families_637.md @@ -0,0 +1,22 @@ +--- +name: selectrow-two-families-637 +description: "#637: TWO unrelated SelectRow families (bare grep over-counts ~10x); blanket TryMakeArchiveRelative in SelectRow would reject every relative/Trash row; ButtonOK_Click does NOT rethrow (delegates to a catch-all ButtonOkClickAsync)" +metadata: + type: project +--- + +Issue #637 research (2026-08-29), branch `bug/breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. + +**Three findings that a single-pass reading gets wrong:** + +1. **`SelectRow` names two unrelated surfaces.** Family A (in scope) = private `BreadcrumbBridgeRouter.SelectRow(BreadcrumbRow)` / `SelectHierarchyPath(BreadcrumbRow, string)` in `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`. Family B = `SelectRow(int index)` on `BreadcrumbStateModel` / `BreadcrumbSelectionSession` / `FolderBreadcrumbBridgeRouter` / `BreadcrumbBridgeCoordinator` (the ItemViewer drop-down selector). A bare grep for `SelectRow` returns ~106 lines / 34 files; only 6 are Family A. Family A has exactly 2 declarations + 7 call sites, ALL in production, ZERO in tests (both members are private; tests drive them via `ProcessInboundAsync` / `SelectFirstRow`). + +2. **A blanket "commit only when `TryMakeArchiveRelative` succeeds" rewrite of `SelectRow` is a REGRESSION.** `TryMakeArchiveRelative` returns FALSE for an already-relative value (`Clients\North`) and for the `Trash to Delete` sentinel, because it is a rooted-prefix test. The change must stay nested inside the existing `ArchiveStemContract.IsFullOutlookPath(selection)` arm. The actual defect is only the negated third conjunct + the missing `stem.Length == 0` clause. + +3. **`EfcFormController.ButtonOK_Click` is `async void` but does NOT rethrow.** It delegates to `ButtonOkClickAsync`, which wraps everything in `catch (System.Exception) { BoundaryErrorSink(...) }` (log-only, injectable seam). The real button-path defect is that `ActionOkAsync` calls `_formViewer.Hide()` BEFORE `await ExecuteMovesAsync()` and `Dispose()/Cleanup()` AFTER, so a throw leaves the form hidden-and-undisposed with no user message. The genuinely unhandled paths are the keyboard ones: the always-on `Keys.Return` action registering `ActionOkAsync` directly, and `KbdExecuteAsync(ActionOkAsync)` for `'K'` — `KbdExecuteAsync` has no try/catch. + +**How to apply:** when an issue body asserts an async-void handler "rethrows", verify the delegated `*Async` body before accepting it; several EFC handlers in this repo follow the delegate-to-a-catch-all pattern. When counting a method family, always disambiguate same-named members on unrelated types first — see [[feedback-exemption-audit-check-proven-techniques]] for the sibling-consistency habit. + +Reusable seam facts: `EfcHomeController.MoveFailureMessageAction` (`internal Action` defaulting to `MessageBox.Show`) is the repo's established injectable abort-notification pattern; `ExecuteMovesAsync`'s existing try/finally is the narrowest seam for a benign degrade. `BreadcrumbBridgeRouterIssue439Tests.cs` is already 694 lines (over the 500 limit) — put new tests elsewhere. + +Full research: docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/research/research.2026-08-29T12-30.md diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/issue.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/issue.md new file mode 100644 index 000000000..472ec4473 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/issue.md @@ -0,0 +1,143 @@ +# breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed (Issue #637) + +- Date captured: 2026-08-26 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed/ (Issue #637) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #637 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/637 +- Last Updated: 2026-08-26 +- Work Mode: full-bug + +## Summary + +Issue #614 established the invariant that `SelectedFolderPath` is an archive-relative stem, and +created `ArchiveStemContract` to express it. The invariant is enforced at the filing boundary, where +`EmailFilerConfig.ResolvePaths` calls `RequireArchiveRelativeStem`, but it is not enforced at the +producer. `BreadcrumbBridgeRouter.SelectRow` still commits a rooted filing target verbatim when that +target is at or under the bound archive root, so a rooted value can still become +`SelectedFolderPath`. That is defect D1 half-closed: the store-root and cross-store leaks are +stopped, but rootedness as such still escapes the producer. + +Two things make this worth fixing rather than tolerating. + +First, it left a live trap that has already fired once. During #614 remediation cycle 1 the OK-path +guard was widened to accept rooted under-root values so that it would agree with `SelectRow`. Because +nothing between the guard and the filing boundary normalizes the value, the accepted value reached +`RequireArchiveRelativeStem` and threw. `ButtonOK_Click` is `async void` and rethrows, and +`ExecuteMovesAsync` wraps its core in try/finally with no catch, so the `ArgumentException` became an +unhandled UI-thread exception after the form had already been hidden. The re-audit caught it and the +change was reverted. The underlying asymmetry that made the widening look reasonable is still +present. + +Second, the D8 normalizer is only half-wired. `EfcDataModel.ToArchiveRelativeStem` exists and is +correct, but it is called only from the `MAPIFolder` overload of `MoveToFolderAsync`. The `string` +overload assigns `DestinationOlStem = folderpath` verbatim, so it performs no normalization at all. +Any rooted value arriving through that overload depends entirely on the boundary throw. + +The fix is to normalize at the producer: in `SelectRow`, when `TryMakeArchiveRelative` succeeds with +a non-empty stem, commit the stem rather than the rooted input; when it succeeds with an empty stem +the value is the archive root itself, which `SelectHierarchyPath` already treats as a deterministic +non-selection and `SelectRow` should too. Once the producer cannot emit a rooted value, the OK guard +and the filing boundary agree by construction rather than by coincidence, and the composition test +added during remediation keeps them agreeing. + +This also requires updating the existing test that asserts a rooted input survives selection, so that +it asserts the stem instead. That is a deliberate spec correction of the same kind #614 already +applied twice, and should be recorded as such rather than treated as a weakened test. + +## Environment + +- OS/version: Windows 11 Pro 10.0.26200; .NET Framework 4.8.1 VSTO add-in. +- Python version: Not applicable; this is C#. +- Command/flags used: Static tracing during the issue #614 remediation re-audit, plus the failing + path reproduced by remediation cycle 1. +- Data source or fixture: Repository source on the issue #614 branch. + +## Steps to Reproduce + +1. Bind breadcrumb rows with an archive root, and present a suggestion row whose filing target is a + rooted path at or under that root. `FolderPredictor.ProjectSuggestionPath` strips the archive + prefix only when the suggestion is strictly under it, so a suggestion whose folder is the archive + root is returned rooted and verbatim. +2. Select that row. `BreadcrumbBridgeRouter.SelectRow` commits the rooted value to + `SelectedFolderPath`. +3. Observe that the value reaching the filing boundary is rooted, and is rejected there by + `RequireArchiveRelativeStem` rather than having been normalized at the producer. + +## Expected Behavior + +`SelectedFolderPath` is always an archive-relative stem. The producer normalizes; the boundary guard +is a backstop that never fires in normal operation. A row whose filing target is the archive root +itself is a non-selection, consistently with `SelectHierarchyPath`. + +## Actual Behavior + +`SelectRow` commits a rooted value verbatim. The invariant is enforced only at the boundary, where +violating it is an exception rather than a corrected value. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: not applicable; established by source tracing. See `BreadcrumbBridgeRouter.SelectRow`, the + `string` overload of `EfcDataModel.MoveToFolderAsync`, and + `ArchiveStemContract.RequireArchiveRelativeStem`. + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +No user-visible defect on the shipped code: a rooted selection is rejected at the OK guard with a +clear dialog. Severity is Medium because the asymmetry is a live trap for future changes, as +demonstrated by remediation cycle 1, and because the half-wired D8 normalizer means one filing +overload relies entirely on a throw. + +## Suspected Cause / Notes + +- `BreadcrumbBridgeRouter.SelectRow` commits the presented filing target verbatim for rooted + at-or-under-root values. +- `EfcDataModel.ToArchiveRelativeStem` is called only from the `MAPIFolder` overload of + `MoveToFolderAsync`; the `string` overload assigns `DestinationOlStem` verbatim. +- The existing issue #439 rooted-target test pins the current producer behavior and will need a + recorded spec correction. +- Related: issue #614 (the parent fix), and its remediation cycle 1, which was reverted for + introducing a crash on this path. + +### Additional finding carried here from the #614 remediation + +The `string` overload of `EfcDataModel.MoveToFolderAsync` reads `Globals.Ol.ArchiveRootPath` inside +the OK chain. After the #614 D6 change that property throws `InvalidOperationException` when the +archive root is unresolvable or cross-store. The chain has no handler: `ExecuteMovesAsync` uses +try/finally with no catch, and `ButtonOK_Click` is `async void` and rethrows, so an unresolvable +archive root is an unhandled UI-thread exception. This was assessed during the #614 remediation and +deliberately not folded into that revert, because a benign degrade requires deciding what aborting a +filing operation should look like to the user and needs its own tests. It belongs with this work +because it is the same defect class on the same chain. + +## Proposed Fix / Validation Ideas + +- [ ] Normalize in `SelectRow`: commit the stem when `TryMakeArchiveRelative` succeeds non-empty; + treat an empty stem as a non-selection, matching `SelectHierarchyPath`. +- [ ] Wire `ToArchiveRelativeStem` into the `string` overload of `MoveToFolderAsync`, or converge the + two overloads on one normalization path. +- [ ] Give the OK-path read of `ArchiveRootPath` a benign degrade so an unresolvable archive root + cannot become an unhandled UI-thread exception. +- [ ] Update the issue #439 rooted-target test to assert the stem, recorded as a deliberate spec + correction. +- [ ] Unit coverage areas: `SelectRow` for rooted under-root, rooted root-exact, rooted out-of-root, + relative, and empty-bound-root inputs; the `string` overload of `MoveToFolderAsync`; the + unresolvable-archive-root degrade. +- [ ] Integration scenario to retest: the composition test added by #614 remediation, asserting that + any value the OK guard accepts does not cause `ResolvePaths` to throw, must still pass. +- [ ] Manual verification notes: select a suggestion row whose folder is the archive root and confirm + it is a deterministic non-selection rather than a rejected selection. + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md new file mode 100644 index 000000000..f8117306b --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -0,0 +1,1338 @@ +# 2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed (Plan) + +- **Issue:** #637 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-08-29T12-20 +- **Status:** Draft +- **Version:** 0.2 +- **Work Mode:** full-bug (from `issue.md`); `spec.md` is the sole acceptance-criteria source (AC1-AC30). + +## Conventions (read before executing any task) + +**FEATURE_DIR** — the feature folder for this issue is +`docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. +Every evidence path in this plan is written relative to FEATURE_DIR +(for example `evidence/baseline/p0-t12-csharpier-check.md` means +`FEATURE_DIR/evidence/baseline/p0-t12-csharpier-check.md`). Commands that require a literal pathspec +or a literal search operand spell the folder path in full rather than using the name FEATURE_DIR, +because a command carrying a placeholder cannot be executed verbatim. The sites that spell it in full +are the git pathspecs described under "Git pathspec scoping" and the token scan in P7-T10. + +**Working directory** — every command below runs with the current directory set to the worktree root +`C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-a68051a23e4479267`. All repository-relative +paths in commands resolve against that root. + +**Base commit** — the diff anchor for this plan is the literal commit +`ecdb1c84ba8541ab67042985919cfed4df768c01`. Every `git diff` in this plan supplies it explicitly. No +task pins a HEAD SHA. + +**Git pathspec scoping** — `.claude/` is a tracked directory in this repository and carries unrelated +in-flight modifications, and `docs/features/parallel/` and `artifacts/` are owned by other processes. +Every `git status --porcelain` and `git diff` gate in this plan is therefore scoped with an explicit +pathspec naming only first-party source, test and feature-document trees. The feature-document +component of every such pathspec is this feature's own folder and never the parent directory +`docs/features/active`. That narrowing is load-bearing rather than cosmetic, and its justification is +forward-looking rather than a claim about the tree as it stands today. At the time this plan was +authored no sibling folder under `docs/features/active` is untracked in this worktree: a +`git status --porcelain` span over that parent directory lists only this feature's own folder, and the +sibling folders are committed — including +`docs/features/active/2026-08-07-breadcrumb-left-right-arrow-parent-child-navigation-440`, whose paths +were confirmed present in this worktree's git index. The narrowing is required regardless, because the +executor runs later than this planning pass: this repository carries several concurrent worktrees and +in-flight feature folders, and a concurrent run in this checkout can leave an untracked or modified +sibling folder under `docs/features/active` at any point between planning and execution. A `git add` +over the parent directory would then stage and commit another feature's folder onto this branch, and a +`git status --porcelain` over the parent directory would report that folder and make every emptiness +gate that consumes it unsatisfiable. This plan does not assume that the tree it observed at planning +time is the tree the executor will meet, so every gate is scoped to paths this plan owns. The default +pathspec is therefore +`-- QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`; +P6-T5 narrows it to `-- QuickFiler QuickFiler.Test` for its post-commit cleanliness check, and P8-T30 +widens it to the nine production and test trees that task audits. No pathspec in this plan names +`.claude/`, `docs/features/parallel/`, `artifacts/`, or the bare `docs/features/active`. +An unscoped gate is unsatisfiable here and must not be substituted. + +**Evidence artifact schema** — every command-step artifact records, as separate lines: +`Timestamp:` (ISO-8601 `yyyy-MM-ddTHH-mm`), `Command:` (the exact command), `EXIT_CODE:`, and +`Output Summary:`. A task whose command is expected to exit non-zero additionally records +`ExpectedExitCode:` with that integer. Baseline and final-QC test artifacts additionally record the +numeric coverage headline values named in their task text. Canonical evidence kinds used by this plan +are `evidence/baseline/`, `evidence/regression-testing/`, `evidence/qa-gates/`, and `evidence/other/`. +`evidence/coverage/` is not a canonical kind and is not used. Nothing under `artifacts/` is used for +evidence. + +**Nullable opt-in token discipline.** No evidence artifact under `evidence/` may spell the literal +token that P7-T10 scans for. Where a task requires recording that the solution-wide nullable opt-in +property is absent from a `Command:` line, the artifact records that fact on a line beginning with +the key `NULLABLE_OPT_IN_PROPERTY:` — the short form the tasks below name is +`NULLABLE_OPT_IN_PROPERTY: absent`, and the long form is +`NULLABLE_OPT_IN_PROPERTY: absent from the recorded Command line`; either satisfies the rule — and +quotes the `Command:` line verbatim, which carries the proof without reproducing the token. The task +text below is the binding form where the two differ. Exactly one artifact cannot satisfy +this rule: `evidence/qa-gates/p7-t10-toolchain-audit.md` must record its own scan command, and that +command's pattern is the token itself. P7-T10's scan is restricted to this feature's own folder, and +within that folder it excludes that single file by an explicit `--glob` exclusion stated in P7-T10; +no other evidence artifact of this feature is excluded from it. Evidence artifacts belonging to other +feature folders lie outside the scan's directory operand entirely, and this plan neither reads nor +changes them. + +**PowerShell invocation form** — every MSBuild and vstest command is issued through +`pwsh -NoProfile -Command '...'` with outer single quotes and inner double quotes. A bare `/m` passed +to a POSIX shell layer is rewritten to a path and MSBuild fails with MSB1008. Every acceptance +condition expressed as a PowerShell expression — including every `(Get-Content -LiteralPath ...).Count` +check in P2-T2, P2-T4, P4-T1, P6-T1 and P6-T3, and every `Test-Path` check in P0-T9 and P0-T11 — is +likewise issued through `pwsh -NoProfile -Command '...'` with outer single quotes and inner double +quotes. Only `git` and `rg` invocations are issued directly. + +**Search invocation form** — every `rg` invocation in this plan is issued with its pattern in single +quotes. `-F` is used only where the pattern is a fixed string whose regex metacharacters — a literal +backslash, or a parenthesis — must match those same characters in the target text, and in that case +a backslash is written once. This plan has exactly two such sites, P2-T3 and P5-T1, and both spell +`-F` in their own task text. Every other `rg` pattern in this plan is a regular expression whose +backslashes are regex escape sequences; those patterns are issued in single quotes without `-F`, +because `-F` would match the escape sequences as literal text and return zero matches for text that +is present. A +pattern that a task below renders in double quotes is issued with that same pattern text enclosed in +single quotes instead; single quotes preserve every backslash exactly, so no pattern below changes +meaning under this rule. The rule is load-bearing because a POSIX shell collapses a doubled backslash +inside double quotes, which turns a written `\\B` into the regex assertion `\B` and returns zero +matches for a literal that is present in the file. + +**Toolchain order** — format, then analyzers, then nullable, then tests. Restart from the format step +whenever a step fails or changes a file. `/t:Rebuild` is mandatory: a warm `/t:Build` exits 0 with +`CoreCompile` skipped on every project, so the analyzer and nullable gates become vacuous. +`/p:Nullable=enable` must never be added: no project carries a `` element and there is no +`Directory.Build.props`, so the property conscripts every unannotated file. +`QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` carries `#nullable enable` at line 1; +`QuickFiler/Controllers/EfcDataModel.cs` does not. + +**Test invocation, verified against `.github/workflows/_mstest-coverage.yml`.** That workflow's test +step (lines 70-86) discovers assemblies under `$env:GITHUB_WORKSPACE` filtered by `\bin\Debug\` and +not `\obj\` and not `\ref\`, and invokes vstest with `/EnableCodeCoverage /InIsolation /Logger:trx` +and `/TestCaseFilter:"TestCategory!=LiveOutlook"`. I verified all three required properties against +that file: + +1. `TestCategory!=LiveOutlook` — present at `_mstest-coverage.yml:83`. Exactly one test method in the + repository carries that category: `TaskMaster.Test/AppGlobals/LiveOutlookHookupIntegrationTests.cs:72`. +2. `/InIsolation` — present at `_mstest-coverage.yml:83`. +3. Workspace-root scoping of assembly discovery — present at `_mstest-coverage.yml:70`. + +Full-suite runs in this plan use the repository wrapper +`scripts/vscode/Invoke-MSTestWithCoverage.ps1`. I read that script and verified its parameters and +its behavior rather than assuming them: + +- Parameters are `-SearchRoot`, `-Configuration`, `-CoverageOutput`, `-NoExecute` + (`Invoke-MSTestWithCoverage.ps1:1-13`, `:248-259`). +- It passes `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook` on the inner vstest call + (`:76`). +- It roots assembly discovery at `$repoRoot`, computed as the script directory plus `..\..` (`:271`), + which for this worktree is the worktree root. That is the local analogue of CI's + `$env:GITHUB_WORKSPACE` scoping and is why a `\.claude\` exclusion filter must **not** be added + here: the worktree root path itself contains `\.claude\`, so such a filter would exclude every + assembly and the run would discover nothing. +- `-SearchRoot .` is passed explicitly on every invocation in this plan. + +Scoped verification runs in this plan call `vstest.console.exe` directly on **one explicitly named +assembly path**, `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, resolved against the worktree root. +That is not a full-assembly discovery search, so no discovery filter can apply to it; naming the single +path is strictly stronger than any filter because it cannot resolve into another worktree. Every such +run still carries `/InIsolation` and conjoins `TestCategory!=LiveOutlook` into its +`/TestCaseFilter`, so its population is comparable to the baseline population restricted to that +assembly. + +**Coverage observables — observed, not inferred.** I read +`scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1` before writing any assertion over its output. +Findings that this plan depends on: + +- The wrapper prints **no** coverage percentage on a successful run. Its success-case stdout literals + are `Post-processing coverage XML for Koverage compatibility...` and `Done. Coverage artifact: ` + (`Invoke-MSTestWithCoverage.ps1:338`, `:344`). Numeric coverage is therefore read from the Cobertura + document, not from stdout. +- `ConvertTo-KoverageCoberturaXml` sets `line-rate`, `branch-rate`, `lines-covered`, `lines-valid`, + `branches-covered` and `branches-valid` on the `/coverage` element (`Helpers.ps1:442-447`; line 441 + is the `Get-CoberturaCoverageSummary` call that produces the values). Those six + attributes are the numeric headline this plan asserts over. +- `Assert-CoberturaLineCoverageThreshold` (`Helpers.ps1:459-491`) throws when the repository line rate + is below 80 percent, and it is called at `Invoke-MSTestWithCoverage.ps1:341`, **before** the + post-processed document is written back at `:343`. A sub-threshold run therefore leaves the on-disk + XML as the raw, unfiltered dotnet-coverage output. The headline reader task below re-applies + `ConvertTo-KoverageCoberturaXml` in memory, which is idempotent on an already-processed document + (path rewriting no longer matches a prefix, package filtering and class merging are no-ops, and the + `sources` node already exists), so it yields the same six numbers in both cases. +- The enforced repository floor in the runner is **80 percent line coverage** (`Helpers.ps1:487-489`: + the percentage is assigned at `:486`, the enforcing comparison `if ($percentage -lt 80)` is at + `:487`, and the `80%` message literal is at `:489`), + which matches CLAUDE.md. `.claude/rules/general-unit-test.md` states 85 percent line and 75 percent + branch. This plan reports the repository-wide figure and treats the runner's own 80 percent gate as + blocking; the change-scoped gates (no changed line loses coverage, new helper fully covered) are + blocking regardless of which repository-wide figure is quoted. The conflict is recorded, not + resolved, by this plan. + +**Formatting observables.** `dotnet tool run csharpier format .` rewrites files and still exits 0, so +its exit code alone proves nothing. The discriminating observation used by this plan is therefore a +before-and-after `git status --porcelain` comparison over the scoped pathspec, taken in the same task +as the write-mode run. I did **not** observe CSharpier 1.2.6's success-case summary wording in this +session, so no acceptance condition in this plan asserts over that wording; every csharpier task +records its stdout verbatim into its artifact for audit and gates only on the exit code and on the +tree observation. `.csharpierignore` excludes `**/evidence/**`, `*.cobertura.xml`, `*.trx`, +`*.csproj`, `*.props` and `*.targets`, so evidence artifacts, coverage documents and the test project +file are outside the formatter's scope. + +**Anchored-diff form.** Before P6-T5 commits, nothing this plan changes is in `HEAD`, so a two-dot +`BASE..HEAD` diff reports nothing for it. Every pre-commit diff gate in this plan therefore uses the +index form `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- ` and is preceded in +the same task by a `git add` over the same paths. Every post-commit diff gate uses +`git diff ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- `. Both forms are anchored to an +explicit ref; the bare unanchored `git diff` is never used. + +**Name-listing diffs carry a companion.** A `git diff --name-only` or `--name-status` enumerates +tracked changes only and never reports an untracked path, so on its own it cannot fail on a file this +plan creates and leaves uncommitted. Every name-listing diff in this plan therefore carries a +`git add` span or a `git status --porcelain` span in the same task, and the task text states what the +executor must observe in that companion output. The two mechanisms are complementary and each alone +is wrong in one state: the anchored diff is blind to untracked files, and porcelain status goes empty +once the change is committed. This plan contains exactly two name-listing diff sites, and both carry a +companion. P6-T5 runs +`git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` +before its diff and asserts that `git status --porcelain -- QuickFiler QuickFiler.Test` produces no +output after the commit. P8-T30 runs a porcelain span over the nine trees it audits, ahead of its two diffs, and +asserts that span is empty; the reconciliation of the two spans at that point in the ordering is +stated in P8-T30 itself. + +**Checkbox-counting declaration (operator constraint 3).** This plan ships **no** tool, script, or +reusable helper that counts checkboxes, criteria, or list items in a generated document. The only +counting of checkboxes it performs is three inline, section-scoped counts, all of them confined to +the `## Acceptance Criteria` section of `spec.md`: P0-T6's baseline count of the acceptance criteria +in that section, and the two independently constructed verifications in P8-T31. Section scoping is +mandatory in all three because `spec.md` genuinely contains five checkboxes outside that section — the Impact/Severity +block at `spec.md:54-57` (four) and the Logs/Screenshots line at `spec.md:86` (one) — so a whole-file +count over-reports by exactly five. Because no reusable tool is introduced, the fixture-test +obligation attached to such a tool does not arise; if a future revision introduces one, that +obligation attaches and must be satisfied before the tool is used. + +## Scope + +In scope, exactly four changes: + +- **A.** `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` — bind the discarded `out _` of + `ArchiveStemContract.TryMakeArchiveRelative` at line 99, commit the stem when non-empty, and treat an + empty stem as a deterministic non-selection with a value-free diagnostic. The change stays nested + inside the existing `ArchiveStemContract.IsFullOutlookPath(selection)` arm. +- **B.** `QuickFiler/Controllers/EfcDataModel.cs` — one new pure `internal static` helper called from + the `DestinationOlStem` assignment at line 287 in the `string` overload of `MoveToFolderAsync`. +- **C.** `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` — one assertion, one test + method name, and one two-line arrange comment, recorded as a deliberate spec correction. +- **D.** Three stale "deferred to issue #637" records. + +Out of scope and owned by issue #695: the `Globals.Ol.ArchiveRootPath` benign degrade, the unhandled +keyboard entry points to `ActionOkAsync`, the half-completed button-path teardown, and the verbatim +`DestinationOlStem` assignments in `EfcDataModel.OpenOlFolderAsync` and `OpenFsFolderAsync`. No task in +this plan touches any of those. + +## Fixed identifiers (the executor does not choose these) + +- New helper: `EfcDataModel.ToFilingStemOrVerbatim(string candidatePath, string archiveAncestor)`, + `internal static string`. +- New test file: `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`, class + `BreadcrumbBridgeRouterIssue637Tests`, with exactly these ten test methods: + `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected`, + `RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection`, + `RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem`, + `RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem`, + `RowSelected_RelativeFilingTarget_CommitsTheValueVerbatim`, + `RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim`, + `RowSelected_OutOfRootRootedTarget_IsStillRejected`, + `RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected`, + `RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim`, + `SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem`. +- New sibling test class `EfcDataModelIssue637Tests`, whose declaration line is written verbatim as + ` public class EfcDataModelIssue637Tests`, matching the form of the existing declaration at + `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs:16`. It is added to that same existing + file, with exactly these eight test methods: + `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem`, + `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem`, + `ToFilingStemOrVerbatim_RelativeStem_ReturnsTheInputVerbatim`, + `ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim`, + `ToFilingStemOrVerbatim_ArchiveRootExact_ReturnsTheInputVerbatimAndDoesNotThrow`, + `ToFilingStemOrVerbatim_OutOfRootRootedInput_ReturnsTheInputVerbatimAndDoesNotThrow`, + `ToFilingStemOrVerbatim_NullEmptyWhitespaceOrSeparatorOnlyAncestor_ReturnsTheInputVerbatim`, + `ToFilingStemOrVerbatim_NullOrEmptyCandidate_ReturnsTheInputVerbatim`. +- Renamed test method (change C): + `Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch` becomes + `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively`. +- New archive-root-exact diagnostic string (change A), value-free and containing no `@`: + `Breadcrumb row rejected: target is the archive root itself.` +- Preserved out-of-root diagnostic string, unchanged: + `Breadcrumb row rejected: target is outside the archive root.` +- Change-D replacement texts, fixed here: + 1. `QuickFiler/Controllers/EfcSelectionGuard.cs:30` becomes + ` /// normalization in BreadcrumbBridgeRouter.SelectRow is implemented by issue #637.` + 2. `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146` becomes + ` // RC-1 inversion: rooted values are never filing stems here; the producer normalizes.` + 3. `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:152` becomes + ` "a rooted value is never a filing stem at this surface and the producer now normalizes before this predicate is reached"` + +## Tree observations recorded while authoring this plan + +These were re-derived against the working tree and one disagrees with `spec.md`. They are recorded so +no downstream artifact inherits a wrong figure. + +1. `QuickFiler/Controllers/EfcDataModel.cs` is **423** lines, not the 424 stated in the spec's + implementation table and in AC25's parenthetical. Headroom to the 500-line limit is 77, not 76. + AC25's binding clause ("at or under 500 lines") is unaffected. +2. The composition test `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` spans + `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:167-214`; AC23 and the spec cite `:167-213`. + The closing brace is at 214. No behavioral consequence. +3. The `#499` clear-on-rebind block spans `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:143-147`; + the spec cites `:143-146`. The write at `:145` and the read at `:143` that AC24 names are exact. +4. Research section 11's claim that no `EfcDataModelTests.cs` exists is wrong; the file exists at 409 + lines. `spec.md` already records this correction and the spec wins. +5. Research section 6's "16 matching lines across 6 files" for the `MoveToFolder` family is 16 lines + across **5** files on the tree. `spec.md` already records this correction and the spec wins. +6. This worktree has no `.dotnet-sdk` directory and no `packages` directory, so the repo-local SDK and + the NuGet package restore must both be bootstrapped before any toolchain command runs. + +### Phase 0 — Context, policy reads, and baseline capture + +- [ ] [P0-T1] Read `CLAUDE.md` in full at the worktree root. Acceptance: the file is read in this + session before any other task in this phase, and its four-step C# toolchain command list is + quoted verbatim into the artifact written by P0-T5. +- [ ] [P0-T2] Read `.claude/rules/general-code-change.md` in full. Acceptance: the file is read, and + its 500-line file-size limit clause is quoted verbatim into the artifact written by P0-T5. +- [ ] [P0-T3] Read `.claude/rules/general-unit-test.md` in full. Acceptance: the file is read, and its + line-coverage and branch-coverage threshold sentence is quoted verbatim into the artifact written + by P0-T5. +- [ ] [P0-T4] Read `.claude/rules/csharp.md` in full. Acceptance: the file is read, and its statement + about the required test framework, mocking library and assertion library is quoted verbatim into + the artifact written by P0-T5. +- [ ] [P0-T5] Write `evidence/baseline/phase0-instructions-read.md` containing `Timestamp:`, + `Policy Order:` naming the four files in the order P0-T1 through P0-T4 read them, an explicit + bulleted list of those four file paths, and the four verbatim quotations required above. + Acceptance: the file exists and contains all of `Timestamp:`, `Policy Order:`, `CLAUDE.md`, + `general-code-change.md`, `general-unit-test.md`, `csharp.md`. +- [ ] [P0-T6] Read `spec.md` in full and write `evidence/baseline/p0-t6-spec-read.md` recording the + count of acceptance criteria found inside the `## Acceptance Criteria` section only. Acceptance: + the recorded count is exactly 30, and the artifact also records that the five checkboxes at + `spec.md:54`, `:55`, `:56`, `:57` and `:86` lie outside that section and are excluded. +- [ ] [P0-T7] Read `research/research.2026-08-29T12-30.md` in full and write + `evidence/baseline/p0-t7-research-read.md` listing the two numbered corrections `spec.md` + records under "Corrections to the research file", the second of which bundles two distinct + file-count facts, and stating that `spec.md` governs where they conflict. + Acceptance: the artifact names the `EfcDataModelTests.cs` existence correction, the + `MoveToFolder` five-file correction, and the `SelectedFolderPath` three-production-file + correction. +- [ ] [P0-T8] Record the branch and base commit. Run + `git rev-parse --abbrev-ref HEAD`, `git rev-parse HEAD`, and + `git merge-base --is-ancestor ecdb1c84ba8541ab67042985919cfed4df768c01 HEAD`, and write + `evidence/baseline/p0-t8-git-base.md`. Acceptance: the `merge-base --is-ancestor` invocation + exits 0, and the recorded branch name is + `bug/breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. If the branch name + differs, record `BRANCH MISMATCH` in the artifact, stop, and report to the orchestrator; do not + proceed to P0-T9. +- [ ] [P0-T9] Bootstrap the repo-local .NET SDK with + `pwsh -NoProfile -File scripts/vscode/Install-RepoDotNetSdk.ps1` and write + `evidence/baseline/p0-t9-sdk-bootstrap.md`. Acceptance: `EXIT_CODE: 0`, and after the run the + path `.dotnet-sdk/dotnet.exe` exists (record the result of `Test-Path .dotnet-sdk/dotnet.exe` as + `True` in `Output Summary:`). `global.json` pins SDK `8.0.205` with `paths` `[".dotnet-sdk", "$host$"]`, + so this step is a prerequisite of every `dotnet` invocation below. If the exit code is non-zero + or the path does not exist, record the captured output under a section headed + `BOOTSTRAP_FAILED:`, stop, and report to the orchestrator; do not proceed to the next task and + do not attempt a repair, because no toolchain command in this plan can run without the + repo-local SDK. +- [ ] [P0-T10] Restore the pinned CSharpier tool with + `pwsh -NoProfile -Command 'dotnet tool restore; "EXIT_CODE=$LASTEXITCODE"'` and write + `evidence/baseline/p0-t10-dotnet-tool-restore.md`. The manifest is `dotnet-tools.json` at the + worktree root and pins `csharpier` `1.2.6`. Acceptance: `EXIT_CODE: 0`, and the captured stdout + is recorded verbatim in the artifact. No assertion is placed on a version banner, because + CSharpier 1.2.6 requires a subcommand and the bare-option form is not a form I have observed + running here; the operative proof that the restore succeeded is that P0-T12's + `dotnet tool run csharpier check .` produces a CSharpier result rather than a tool-resolution + error, which P0-T12 records. If the exit code is non-zero, record the captured output under a + section headed `BOOTSTRAP_FAILED:`, stop, and report to the orchestrator; do not proceed to the + next task and do not attempt a repair. +- [ ] [P0-T11] Restore NuGet packages with + `pwsh -NoProfile -File scripts/vscode/Invoke-Restore.ps1` and write + `evidence/baseline/p0-t11-nuget-restore.md`. This script resolves MSBuild through vswhere and + runs `/t:Restore /p:RestorePackagesConfig=true`; it does not rewrite any `.csproj` HintPath. + Acceptance: `EXIT_CODE: 0`, and after the run the directory `packages` exists (record + `Test-Path packages` as `True`). If the exit code is non-zero or the directory does not exist, + record the captured output under a section headed `BOOTSTRAP_FAILED:`, stop, and report to the + orchestrator; do not proceed to the next task and do not attempt a repair, because an + unrestored package graph produces CS0006 reference errors that are indistinguishable from real + analyzer findings. +- [ ] [P0-T12] Capture the baseline format state **read-only** with + `pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"'` and + write `evidence/baseline/p0-t12-csharpier-check.md`. The write-mode `format` command must not be + run in Phase 0: repairing pre-existing drift before the baseline would either waive it silently + or make a later zero-diff gate unsatisfiable. Acceptance: the artifact records `EXIT_CODE:`, the + captured stdout verbatim, and a section headed `BASELINE_FORMAT_DRIFT:` listing every file path + the captured output names as needing formatting (the list is empty when `EXIT_CODE: 0`, and a + non-zero exit code is the signal that the list is non-empty). Later zero-diff formatting gates + exclude exactly the paths in that list and nothing else. The artifact also records whether the + invocation produced a CSharpier result at all, which is the proof that P0-T10's tool restore + succeeded. +- [ ] [P0-T13] Capture the baseline analyzer build. Run + `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; "EXIT_CODE=$LASTEXITCODE"'` + and write `evidence/baseline/p0-t13-msbuild-analyzers.md`. Acceptance: `EXIT_CODE: 0`; the + artifact records the MSBuild final status line and the `Warning(s)` and `Error(s)` counts as + printed; the captured output contains the literal `(Rebuild target(s))` at least once, which is + the per-project completion line MSBuild emits for the Rebuild target and is therefore the + discriminator against a skipped incremental Build; and the recorded `Command:` line does not + contain the solution-wide nullable opt-in property — record this as + `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. If the exit code is + non-zero, record the full diagnostic list under a section headed `BASELINE_BUILD_RED:`, stop, + and report to the orchestrator; do not proceed to the next task and do not attempt a repair, + because a pre-existing red baseline is outside this plan's scope. +- [ ] [P0-T14] Capture the baseline nullable build. Run + `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true; "EXIT_CODE=$LASTEXITCODE"'` + and write `evidence/baseline/p0-t14-msbuild-nullable.md`. Acceptance: `EXIT_CODE: 0`; the + captured output contains `(Rebuild target(s))`; and the recorded `Command:` line contains + `/t:Rebuild` and does not contain the solution-wide nullable opt-in property — record this as + `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. If the exit code is + non-zero, record the full diagnostic list under a section headed `BASELINE_BUILD_RED:`, stop, + and report to the orchestrator; do not proceed to the next task and do not attempt a repair, + because a pre-existing red baseline is outside this plan's scope. +- [ ] [P0-T15] Capture the baseline full test run with coverage. Run + `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p0-t15-baseline.cobertura.xml` + and write `evidence/baseline/p0-t15-mstest-coverage.md`. `coverage/*` is gitignored + (`.gitignore:144`), so the Cobertura document does not dirty the tree. Acceptance: the artifact + records `EXIT_CODE:`, the number of discovered test assemblies printed by the wrapper, the total + and passed and failed test counts, and a section headed `BASELINE_FAILURE_SET:` naming every + failing test's fully qualified name (empty when the run passes). The file + `coverage/p0-t15-baseline.cobertura.xml` exists after the run. `Output Summary:` additionally + carries the six numeric `/coverage` attribute values and the derived line and branch + percentages that P0-T16 reads, copied in once P0-T16 has produced them; this task is not + complete until that copy-back has been made, because the plan contract requires the baseline + test-step artifact itself to carry the numeric coverage headline. +- [ ] [P0-T16] Read the baseline numeric coverage headline. Run + `pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; $c = $d.SelectSingleNode("/coverage"); foreach ($a in @("line-rate","branch-rate","lines-covered","lines-valid","branches-covered","branches-valid")) { $a + "=" + $c.GetAttribute($a) }'` + and write `evidence/baseline/p0-t16-coverage-headline.md`. Acceptance: `EXIT_CODE: 0`, and + `Output Summary:` records all six numeric values, plus the derived baseline line-coverage + percentage computed as `line-rate` multiplied by 100 and the derived branch percentage computed + as `branch-rate` multiplied by 100. These are the baseline figures the Phase 7 delta task + compares against. +- [ ] [P0-T17] Record the baseline uncovered-line sets for the two production files this plan changes. + Run + `pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; foreach ($f in @("QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcDataModel.cs")) { $u = @(); foreach ($c in $d.SelectNodes("//class")) { if ($c.GetAttribute("filename") -eq $f) { foreach ($l in $c.SelectNodes("./lines/line")) { if ([int]$l.GetAttribute("hits") -eq 0) { $u += [int]$l.GetAttribute("number") } } } }; $f + " uncovered=" + (($u | Sort-Object -Unique) -join ",") } '` + and write `evidence/baseline/p0-t17-baseline-uncovered-lines.md`. Acceptance: `EXIT_CODE: 0`, and + the artifact records one `uncovered=` line for each of the two file paths, even when the set is + empty. + +### Phase 1 — Pre-change census re-derivation, two independent searches per number + +Every number in this phase is a number that `spec.md` already carries inside an approved acceptance +criterion. Each task verifies it by a search over the full symbol family and cross-checks it with a +second, independently constructed search. No number in this phase is verified by a single-pass grep. + +- [ ] [P1-T1] Re-derive the selection family census (AC9: 2 declarations, 7 call sites). + Search 1, path-anchored: `rg -n "Select(Row|HierarchyPath)\s*\(" --glob "*.cs" QuickFiler/Controllers/BreadcrumbBridgeRouter.cs QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs`. + Search 2, syntax-anchored and independently constructed on unqualified-invocation form rather + than on file paths: `rg -n "^\s+(private void )?Select(Row|HierarchyPath)\(" --glob "*.cs" .` + run over the whole repository. Search 2 excludes the unrelated Family-B `SelectRow(int index)` + surface on `BreadcrumbStateModel`, `BreadcrumbSelectionSession`, `FolderBreadcrumbBridgeRouter` + and `BreadcrumbBridgeCoordinator`, because every Family-B site carries a receiver or a + non-`private void` modifier. A naive `rg "SelectRow"` returns roughly ten times too many lines + and must not be used. Write `evidence/baseline/p1-t1-selection-family.md`. Acceptance: both + searches return exactly 9 lines; the two line sets are identical; the artifact lists them and + classifies each as declaration or call; declarations total 2 + (`BreadcrumbBridgeRouter.Selection.cs:83` and `:109`) and call sites total 7 + (`BreadcrumbBridgeRouter.cs:201`, `:286`, `BreadcrumbBridgeRouter.Arrows.cs:138`, `:153`, `:161`, + `BreadcrumbBridgeRouter.Selection.cs:33`, `:47`). +- [ ] [P1-T2] Re-derive the `MoveToFolder` family census (AC16: 3 declarations, 6 call sites). + Search 1, family-stem: `rg -n "MoveToFolder" --glob "*.cs" .` — the bare stem catches any + non-`Async` sibling or partially renamed overload that an `Async`-suffixed pattern would miss. + Search 2, independently constructed on invocation and declaration syntax: + `rg -n "MoveToFolderAsync\s*\(" --glob "*.cs" .` — this excludes the `MoveToFolderAsyncAction` + delegate property, its null test and its invocation, which are textual references rather than + family members. Write `evidence/baseline/p1-t2-movetofolder-family.md`. Acceptance: Search 1 + returns 16 lines across 5 files; Search 2 returns 9 lines across 4 files; the artifact classifies + Search 2's 9 lines as exactly 3 declarations (`EfcDataModel.cs:259`, `EfcDataModel.cs:336`, + `EfcHomeController.ExecuteMoves.cs:89`) and 6 call sites + (`EfcHomeController.ExecuteMoves.cs:78`, `:98`, `EfcDataModel.cs:346`, `EfcFormController.cs:537`, + `:844`, `EfcHomeControllerExecuteMovesTests.cs:87`); and the artifact records that Search 1 minus + Search 2 leaves exactly 7 non-member textual references, closing the 16-line accounting. The + artifact also records that the file count is 5, not the 6 stated in research section 6. +- [ ] [P1-T3] Re-derive the `SelectedFolderPath` surface (AC24: 9 lines across 3 production files, 2 + writes, 3 reads). Search 1: `rg -c "SelectedFolderPath" --glob "*.cs" .`, recording the per-file + counts. Search 2, independently constructed by scoping to the production project directories up + front rather than by subtracting the test projects from Search 1's table: + `rg -n "SelectedFolderPath" --glob "*.cs" QuickFiler/ UtilitiesCS/ TaskMaster/ ToDoModel/ Tags/ TaskVisualization/`. + Write + `evidence/baseline/p1-t3-selectedfolderpath-surface.md`. Acceptance: Search 1 returns 74 lines + across 9 files; Search 2 returns exactly 9 lines across exactly 3 files + (`BreadcrumbBridgeRouter.cs`, `BreadcrumbBridgeRouter.Selection.cs`, `EfcFormController.cs`); the + artifact classifies those 9 lines as 1 declaration (`BreadcrumbBridgeRouter.cs:59`), 1 doc + reference (`:61`), 2 writes (`:145` and `BreadcrumbBridgeRouter.Selection.cs:134`), 3 reads + (`:143`, `BreadcrumbBridgeRouter.Selection.cs:138`, `EfcFormController.cs:321`) and 2 event-only + lines (`:62`, `:146`); and it records that the production split is 3 production files and 6 test + files, not the 2-and-7 stated in research section 7. +- [ ] [P1-T4] Re-derive the stale deferral record census (AC22: 3 records). + Search 1, on the deferral phrase: `rg -n "deferred to issue #637" --glob "*.cs" .`. + Search 2, independently constructed on the issue reference alone so it cannot miss a differently + worded deferral: `rg -n "#637" --glob "*.cs" .`. Write + `evidence/baseline/p1-t4-deferral-records.md`. Acceptance: Search 1 returns exactly 3 lines + (`QuickFiler/Controllers/EfcSelectionGuard.cs:30`, + `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146`, and `:152`); Search 2 returns a + superset whose every additional line is enumerated in the artifact and individually classified as + not a deferral claim; and the artifact quotes the current text of all three Search 1 lines + verbatim. +- [ ] [P1-T5] Re-derive the existing `ToArchiveRelativeStem` test count (AC15: 8 tests). + Search 1, on the method-name convention: + `rg -n "public void ToArchiveRelativeStem_" QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`. + Search 2, independently constructed on the call to the member under test rather than on test + naming: `rg -n "EfcDataModel\.ToArchiveRelativeStem\(" QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`. + Write `evidence/baseline/p1-t5-toarchiverelativestem-tests.md`. Acceptance: both searches return + exactly 8 lines; the artifact records the declaration line numbers 21, 34, 48, 62, 72, 87, 100, + 111 and records that `ToArchiveRelativeStem_ArchiveRootItself_Throws` is the method at line 62. +- [ ] [P1-T6] Re-derive the no-bound-root pass-through test pair (AC4: 2 tests). + Search 1, by name: + `rg -n "Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection|SegmentActivate_WithNoBoundArchiveRoot_PreservesThePassThroughMode" --glob "*.cs" .`. + Search 2, independently constructed on the binding mechanism that produces an empty bound root — + a separator-only fourth argument or the three-argument public overload: + `rg -n "BindRowsAsync\(" --glob "*.cs" QuickFiler.Test/Controllers/` with each hit classified by + whether it supplies an archive root. Write `evidence/baseline/p1-t6-passthrough-tests.md`. + Acceptance: Search 1 returns exactly 2 declaration lines + (`BreadcrumbBridgeRouterIssue439Tests.cs:619` and `BreadcrumbBridgeRouterIssue614Tests.cs:188`); + the artifact records that the first binds `@"\"` at `:645` and asserts `Be(@"\Archive")` at + `:665`, and the second uses the three-argument overload at `:213` and asserts `Be(@"\Archive")` at + `:221`; and Search 2's classification identifies the same two tests as the only pass-through + cases and no others. +- [ ] [P1-T7] Re-derive the file line counts AC25 depends on. Construction 1: + `pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcSelectionGuard.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs","QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }'`. + Construction 2, independently constructed with a line-oriented search rather than a file read: + `rg -c "^" --glob "*.cs" QuickFiler/Controllers/ QuickFiler.Test/Controllers/` filtered to the + same six paths. `Measure-Object -Line` must not be substituted for `(Get-Content).Count`; it + reports a different figure for a file without a trailing newline. Write + `evidence/baseline/p1-t7-file-line-counts.md`. Acceptance: both constructions agree on all six + paths; `EfcDataModel.cs` is 423; `BreadcrumbBridgeRouter.Selection.cs` is 209; + `EfcSelectionGuard.cs` is 79; `BreadcrumbBridgeRouterIssue439Tests.cs` is 694; + `EfcDataModelIssue614Tests.cs` is 123; `EfcSelectionGuardTests.cs` is 296; and the artifact + records that `spec.md` states 424 for `EfcDataModel.cs` and that the tree value 423 governs. +- [ ] [P1-T8] Re-derive the single pinning assertion (AC20: exactly 1 existing assertion changes). + Construction 1, on the assertion form: + `rg -n "SelectedFolderPath\.Should\(\)\.Be\(" --glob "*.cs" QuickFiler.Test/` with every hit + classified by whether the selected row's filing target is a full Outlook path at or under a + non-empty bound root. Construction 2, independently constructed on the trigger side rather than + the assertion side: `rg -n "rowSelected|SelectFirstRow" --glob "*.cs" QuickFiler.Test/` with + every hit classified by its bound root and its presented filing target. Write + `evidence/baseline/p1-t8-pinning-assertion.md`. Acceptance: both constructions identify exactly + one assertion that must change — `BreadcrumbBridgeRouterIssue439Tests.cs:165`, + `router.SelectedFolderPath.Should().Be(fullTarget);` inside the method declared at `:119` — and + the artifact records that no test anywhere binds a presented row whose filing target equals the + bound archive root, so zero tests depend on the archive-root-exact case being a selection. + +### Phase 2 — Regression tests that fail before the fix + +The change-B tests name a member that does not exist yet, and a test file referencing a missing member +makes the whole `QuickFiler.Test` assembly fail to compile, which would prevent every other test in +this phase from running at all. P2-T1 therefore lands a behavior-preserving seam first: the helper is +declared and called, but returns its input verbatim, which is byte-for-byte the behavior of the current +assignment at `EfcDataModel.cs:287`. The red in this phase is a genuine runtime red, not a compile +failure. + +- [ ] [P2-T1] Add the behavior-preserving seam. In `QuickFiler/Controllers/EfcDataModel.cs`, declare + `internal static string ToFilingStemOrVerbatim(string candidatePath, string archiveAncestor)` + whose body is exactly + `_ = ArchiveStemContract.TryMakeArchiveRelative(candidatePath, archiveAncestor, out _);` followed + by `return candidatePath;`, with an XML documentation comment stating that this is the #637 seam + and that the normalization lands in P4-T1. Change the assignment at `EfcDataModel.cs:287` from + `DestinationOlStem = folderpath,` to + `DestinationOlStem = ToFilingStemOrVerbatim(folderpath, Globals.Ol.ArchiveRootPath),`. + The explicit discard on the `TryMakeArchiveRelative` call is required so that both parameters are + used and no unused-parameter diagnostic can be promoted to an error by + `/p:TreatWarningsAsErrors=true`. Declare the helper immediately after the closing brace of the + `string` overload of `MoveToFolderAsync` at original line 297 and before + `internal async Task OpenOlFolderAsync` at original line 299, so the insertion hunk falls + outside both ranges P4-T6 excludes. Do not place it adjacent to `ToArchiveRelativeStem`. The + seam's XML documentation must not contain the token `MoveToFolder`; refer to its caller as + "the `string` filing overload" instead, because P8-T16 asserts the family stem search still + returns exactly 16 lines. For the same reason the seam's XML documentation must not contain + either of the two literals this task asserts an exact count of 1 for — + `internal static string ToFilingStemOrVerbatim` and `DestinationOlStem = ToFilingStemOrVerbatim` + — so it must not reproduce the declaration signature or the assignment statement; naming the + method by its bare identifier is permitted and is classified rather than counted by P4-T2. + Acceptance: `rg -n "internal static string ToFilingStemOrVerbatim" QuickFiler/Controllers/EfcDataModel.cs` + returns exactly 1 line, and + `rg -n "DestinationOlStem = ToFilingStemOrVerbatim" QuickFiler/Controllers/EfcDataModel.cs` + returns exactly 1 line. +- [ ] [P2-T2] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` containing + the class `BreadcrumbBridgeRouterIssue637Tests` with exactly the ten test methods named in the + "Fixed identifiers" section. Use fixture Shape 2 and do not invent a new fixture shape: a + `[TestInitialize]` `Setup` and `[TestCleanup]` `Cleanup` modelled on + `BreadcrumbBridgeRouterIssue614Tests.cs:38-58`, the log4net `MemoryAppender` attach and detach + helpers modelled on `:338-356`, the `Key` and `Segment` helpers modelled on `:328-336`, the + `Inbound` and `RowSelected` JSON helpers modelled on `:264-267` and `:288-291`, the + `RenderedMessages` and `AssertRejectionDiagnosticWithoutIdentifiers` helpers modelled on + `:304-326`, and a `BindRows` helper modelled on `BindChain` at `:236-262` that accepts an archive + root plus one or more presented row texts and sets up `ResolveLeafKeyAsync` and + `GetAncestorChainAsync` for every presented row that `BreadcrumbRowBuilder.Classify` treats as a + suggestion. To produce an empty bound root, pass the separator-only value `@"\"`, which + `BindRowsAsync` trims to empty at `BreadcrumbBridgeRouter.cs:107-109`. Framework is MSTest with + Moq and FluentAssertions; no temporary file, no wall-clock wait, no Outlook process. No text in + this file may contain the token `MoveToFolder`, because P8-T16 asserts that a repository-wide + `*.cs` search for that stem still returns exactly 16 lines. Acceptance: + the file exists; `rg -c "\[TestMethod\]" QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` + returns 10; each of the ten fixed method names is found exactly once by + `rg -n "public void " QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`; and + `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs").Count` + is at most 500. +- [ ] [P2-T3] Register the new test file in the non-SDK project. Insert + ` ` into + `QuickFiler.Test/QuickFiler.Test.csproj` immediately after the existing line 64, + ` `. A file absent + from this project compiles into nothing and its tests silently never run. The literal this task + creates is `Controllers\BreadcrumbBridgeRouterIssue637Tests.cs`, quoted here verbatim because it + is absent from the tracked tree until this task inserts it. The acceptance search below is the + fixed-string, single-quoted form required by the "Search invocation form" convention: the + backslash is written once and `-F` disables regex interpretation, so no shell layer and no + regex engine can consume it. Acceptance: + `rg -F -n 'Controllers\BreadcrumbBridgeRouterIssue637Tests.cs' QuickFiler.Test/QuickFiler.Test.csproj` + returns exactly 1 line, and that line is inside the same `ItemGroup` that begins at line 57. +- [ ] [P2-T4] Add the change-B helper tests. In the existing file + `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, add a new sibling `[TestClass]` + `EfcDataModelIssue637Tests` containing exactly the eight test methods named in the "Fixed + identifiers" section, reaching `EfcDataModel.ToFilingStemOrVerbatim` through the existing + `InternalsVisibleTo("QuickFiler.Test")` at `QuickFiler/Properties/AssemblyInfo.cs:5`. The eight + existing `ToArchiveRelativeStem` tests in the file are not modified. The file is already + registered at `QuickFiler.Test/QuickFiler.Test.csproj:114`, so no new `Compile Include` is + required. The declaration line this task creates is + ` public class EfcDataModelIssue637Tests`, matching the form of the existing declaration at + `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs:16`. The acceptance search below + therefore asserts over the literal `class EfcDataModelIssue637Tests`, which is quoted here + verbatim because it is absent from the tracked tree until this task creates it. No text this + task adds may contain the token `MoveToFolder`, because P8-T16 asserts that a repository-wide + `*.cs` search for that stem still returns exactly 16 lines. Acceptance: + `rg -n "class EfcDataModelIssue637Tests" QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` + returns exactly 1 line; each of the eight fixed method names is found exactly once in that file; + `git add QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` followed in the same task by + `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` + shows zero removed content lines, meaning zero lines beginning with a single `-`; and + `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs").Count` is + at most 500. +- [ ] [P2-T5] Run the analyzer build and write `evidence/regression-testing/p2-t5-msbuild-analyzers.md` + using the P0-T13 command verbatim. Acceptance: `EXIT_CODE: 0`; the output contains + `(Rebuild target(s))`; and the `Error(s)` count is 0. A non-zero exit here means the seam or the + new test files do not compile and must be repaired before P2-T7 runs. +- [ ] [P2-T6] Run the nullable build and write `evidence/regression-testing/p2-t6-msbuild-nullable.md` + using the P0-T14 command verbatim. Acceptance: `EXIT_CODE: 0`; the output contains + `(Rebuild target(s))`; the recorded `Command:` line does not contain the solution-wide nullable + opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in + the artifact. This gate is where an unused-parameter or nullable diagnostic introduced by the + seam would surface as an error. +- [ ] [P2-T7] [expect-fail] Run the new router regression tests before the fix. Run + `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; $asm = Join-Path (Get-Location).Path "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll"; & $vstest $asm /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue637Tests&TestCategory!=LiveOutlook" /Logger:trx "/ResultsDirectory:coverage\testresults\p2-t7"; "EXIT_CODE=$LASTEXITCODE"'` + and write `evidence/regression-testing/p2-t7-router-tests-red.md` with `ExpectedExitCode: 1`. + Acceptance: the output does not contain `No test matches the given testcase filter`; the run + reports 10 tests total; exactly these 5 fail, named individually in the artifact: + `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected`, + `RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection`, + `RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem`, + `RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem`, + `SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem`; and exactly these 5 + pass: `RowSelected_RelativeFilingTarget_CommitsTheValueVerbatim`, + `RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim`, + `RowSelected_OutOfRootRootedTarget_IsStillRejected`, + `RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected`, + `RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim`. A different partition is a + defect in the tests, not evidence of the bug, and must be repaired before Phase 3. +- [ ] [P2-T8] [expect-fail] Run the new helper tests before the fix. Run the P2-T7 command with the + filter substring changed to `FullyQualifiedName~EfcDataModelIssue637Tests` and the results + directory changed to `coverage\testresults\p2-t8`, and write + `evidence/regression-testing/p2-t8-helper-tests-red.md` with `ExpectedExitCode: 1`. Acceptance: + the output does not contain `No test matches the given testcase filter`; the run reports 8 tests + total; exactly these 2 fail: `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` and + `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem`; and the other 6 pass, + because the seam already returns the input verbatim for every non-normalizable case. +- [ ] [P2-T9] Prove the new test file actually executes rather than silently compiling into nothing. + From the TRX produced by P2-T7 at `coverage\testresults\p2-t7`, extract every `UnitTestResult` + whose `testName` begins with one of the ten fixed method names, and write + `evidence/regression-testing/p2-t9-compile-include-observed.md`. Acceptance: the artifact records + exactly 10 such results; it quotes the `Compile Include` line added by P2-T3 verbatim; and it + records that removing that line would make this count 0, which is the observable AC26 requires. + +### Phase 3 — Change A, producer normalization in `SelectRow` + +- [ ] [P3-T1] Apply change A in `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`. Replace + the guard currently at lines 94-106 so that the `_boundRoot.Length != 0` and + `ArchiveStemContract.IsFullOutlookPath(selection)` conjunction opens a block; inside that block, + a failed `ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out string stem)` + logs the unchanged message `Breadcrumb row rejected: target is outside the archive root.` and + returns; a succeeded call with `stem.Length == 0` logs the new message + `Breadcrumb row rejected: target is the archive root itself.` and returns; otherwise `selection` + is reassigned to `stem`. Control then falls through to the unchanged + `CommitSelection(row, selection);`. `selection` is a non-nullable `string` and `stem` is + definitely assigned by `ArchiveStemContract.cs:112` on every exit path, so no nullable temporary + is introduced and `CommitSelection`'s non-nullable `string` parameter is satisfied. Replace the + stale comment at lines 94-95, which asserts the superseded pass-verbatim behavior. The + replacement comment must not contain the token `IsFullOutlookPath`; refer to the arm as the + full-path gate instead, because this task's acceptance asserts an exact count of 1 for that + token in this file. For the same reason the replacement comment must not contain the token + `out string stem`, whose exact count of 2 this task also asserts, nor the token `out _`, whose + count this task asserts is 0, nor either of the two diagnostic message literals whose exact + count of 1 apiece P3-T2 asserts over this same file: describe the two rejection outcomes in + prose without quoting their message text. Acceptance: + `rg -n "out string stem" QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` returns 2 + lines (the new one in `SelectRow` and the existing one at `:120` in `SelectHierarchyPath`); + `rg -n "out _" QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` returns 0 lines; and + `rg -c "IsFullOutlookPath" QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` returns 1. +- [ ] [P3-T2] Verify the nesting and the preserved diagnostics required by AC3 and AC6. Acceptance: + `rg -n "Breadcrumb row rejected: target is outside the archive root." QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` + returns exactly 1 line; + `rg -n "Breadcrumb row rejected: target is the archive root itself." QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` + returns exactly 1 line; neither message contains the character `@`; and the artifact + `evidence/regression-testing/p3-t2-nesting.md` quotes the whole edited `SelectRow` body and + records that `_boundRoot.Length != 0` is still the first conjunct, so the no-bound-root + pass-through mode is untouched. +- [ ] [P3-T3] Verify AC8: `SelectHierarchyPath` and `CommitSelection` are unmodified. Run + `git add QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` then + `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` + and write `evidence/regression-testing/p3-t3-selectionfile-diff.md`. Acceptance: every hunk + header in the diff addresses a line range that lies entirely within the original lines 83 to 107; + no hunk touches the original line range 109 to 139; and the artifact lists the hunk headers + verbatim. +- [ ] [P3-T4] Run the analyzer build and the nullable build using the P0-T13 and P0-T14 commands + verbatim, and write `evidence/regression-testing/p3-t4-builds.md` recording both. Acceptance: + both record `EXIT_CODE: 0`; both outputs contain `(Rebuild target(s))`; and neither recorded + `Command:` line contains the solution-wide nullable opt-in property — record this as + `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. +- [ ] [P3-T5] Run the router regression suite green. Use the P2-T7 command with the results directory + changed to `coverage\testresults\p3-t5`, and write + `evidence/regression-testing/p3-t5-router-tests-green.md`. Acceptance: `EXIT_CODE: 0`; 10 tests + total; 10 passed; 0 failed; 0 skipped; and the five tests that failed in P2-T7 are named + individually in the artifact as now passing. +- [ ] [P3-T6] Run the unmodified router test classes to prove no collateral regression. Use the P2-T7 + command with the filter + `"/TestCaseFilter:(FullyQualifiedName~BreadcrumbBridgeRouterIssue614Tests|FullyQualifiedName~BreadcrumbBridgeRouterTests|FullyQualifiedName~BreadcrumbBridgeRouterQueueTests)&TestCategory!=LiveOutlook"` + and the results directory `coverage\testresults\p3-t6`, and write + `evidence/regression-testing/p3-t6-router-siblings.md`. Acceptance: `EXIT_CODE: 0`; 0 failed; and + the artifact records that `RowSelected_OutOfRootFilingTarget_DoesNotStoreAFullOutlookPath` and + `SegmentActivate_WithNoBoundArchiveRoot_PreservesThePassThroughMode` both passed. This filter + deliberately excludes `BreadcrumbBridgeRouterIssue439Tests`, whose pinning assertion is expected + to be red between P3-T1 and P5-T1; that class is run green in P5-T6. + +### Phase 4 — Change B, normalization in the `string` overload of `MoveToFolderAsync` + +- [ ] [P4-T1] Replace the seam body in `QuickFiler/Controllers/EfcDataModel.cs` with the real + normalization. `ToFilingStemOrVerbatim` returns `candidatePath` unchanged when + `ArchiveStemContract.IsFullOutlookPath(candidatePath)` is false; otherwise it calls + `ArchiveStemContract.TryMakeArchiveRelative(candidatePath, archiveAncestor, out string stem)` and + returns `stem` when that call succeeds and `stem.Length != 0`, and returns `candidatePath` + unchanged in every other case. The method is total, never throws, performs no I/O, writes no log, + and touches no static mutable state. It deliberately does not adopt + `ToArchiveRelativeStem`'s throw on the archive-root-exact input; the rationale is recorded in + `spec.md` under "Error handling and logging updates". Update the XML documentation to state the + final contract and remove the seam wording added by P2-T1. The helper's XML documentation must + not contain the token `MoveToFolder`; refer to its caller as "the `string` filing overload" + instead, because P8-T16 asserts the family stem search still returns exactly 16 lines. Three + further tokens are barred from that documentation for the same reason — each is a token some + acceptance condition asserts an exact count for over this same file, and the natural wording of + the contract would otherwise add an occurrence. First, `IsFullOutlookPath`: AC12 phrases the + contract as "The helper is gated on `ArchiveStemContract.IsFullOutlookPath`", but this task + asserts an exact count of 1 for that token in this file, so the documentation states the gate as + "returns its input unchanged unless the input is a full Outlook path" without naming the + predicate. Second, the character sequence `throw` in any form, including `throws`: this task + compares the matched line texts of `rg -n "throw"` taken before the edit against those taken + after it and requires the two sets identical, so the totality claim is worded as + "returns a value for every input and + propagates no exception". Third, `Globals.Ol.ArchiveRootPath`: P4-T6 asserts an exact count of 4 + for that token in this file, so the documentation describes the second parameter as the archive + ancestor supplied by the caller rather than naming the global. Record + the run in + `evidence/regression-testing/p4-t1-helper-implemented.md`, capturing the output of + `rg -n "throw" QuickFiler/Controllers/EfcDataModel.cs` taken immediately before and immediately + after the edit. Acceptance: the two `rg` outputs contain the identical set of matched line + **texts**, compared without their line numbers because the helper body changes length and shifts + every later line number, so the helper introduces no new throw site; + `rg -n "IsFullOutlookPath" QuickFiler/Controllers/EfcDataModel.cs` returns exactly 1 line and it + is inside the helper, where before this task it returned 0 lines; and + `(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.cs").Count` is at most 500. +- [ ] [P4-T2] Record the helper's line range and verify its purity, and write + `evidence/regression-testing/p4-t2-helper-shape.md`. Acceptance: the artifact records the first + and last line numbers of the `ToFilingStemOrVerbatim` declaration body; it records that the body + contains no `await`, no `Globals`, no `logger`, and no `throw`; it records that the only call + sites of the helper are the single assignment in the `string` overload and the eight tests in + `EfcDataModelIssue637Tests`, verified by `rg -n "ToFilingStemOrVerbatim" --glob "*.cs" .`; and it + enumerates every line `rg -n "ToFilingStemOrVerbatim" --glob "*.cs" QuickFiler/` returns and + classifies each as the single declaration, the single call, or an XML-documentation reference, + with exactly one declaration and exactly one call. A second call site anywhere in `QuickFiler/` + fails this task. +- [ ] [P4-T3] Run the analyzer build and the nullable build using the P0-T13 and P0-T14 commands + verbatim, and write `evidence/regression-testing/p4-t3-builds.md`. Acceptance: both record + `EXIT_CODE: 0`; both outputs contain `(Rebuild target(s))`; and neither recorded `Command:` line + contains the solution-wide nullable opt-in property — record this as + `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. +- [ ] [P4-T4] Run the helper test class green. Use the P2-T8 command with the results directory + changed to `coverage\testresults\p4-t4`, and write + `evidence/regression-testing/p4-t4-helper-tests-green.md`. Acceptance: `EXIT_CODE: 0`; 8 tests + total; 8 passed; 0 failed; and the two tests that failed in P2-T8 are named individually as now + passing. +- [ ] [P4-T5] Prove the eight existing `ToArchiveRelativeStem` tests are unchanged and still pass. Use + the P2-T7 command with the filter + `"/TestCaseFilter:FullyQualifiedName~EfcDataModelIssue614Tests&TestCategory!=LiveOutlook"` and the + results directory `coverage\testresults\p4-t5`, and write + `evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md`. Acceptance: + `EXIT_CODE: 0`; the run reports 8 tests for that class; 8 passed including + `ToArchiveRelativeStem_ArchiveRootItself_Throws`; and, after + `git add QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, + `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` + shows zero removed content lines. +- [ ] [P4-T6] Verify AC17: the non-goals are untouched. Run + `git add QuickFiler/Controllers/EfcDataModel.cs` then + `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -U0 -- QuickFiler/Controllers/EfcDataModel.cs` + and write `evidence/regression-testing/p4-t6-nongoals-untouched.md`. Acceptance: no hunk header + addresses any line inside the original ranges 299 to 334 (`OpenOlFolderAsync` and + `OpenFsFolderAsync`) or 336 to 386 (the `MAPIFolder` overload and `ToArchiveRelativeStem`); a + pure-insertion hunk whose old-side range is `-297,0` or `-298,0` is the helper declaration + required by P2-T1 and is expected; any other hunk outside the line-287 assignment fails this + task; and `rg -n "Globals.Ol.ArchiveRootPath" QuickFiler/Controllers/EfcDataModel.cs` returns + exactly 4 lines, quoted in the artifact and classified as the 3 pre-existing `OlAncestor` + initializers (originally lines 289, 310 and 328, shifted by the length of the helper this plan + adds) plus the single new argument on the `DestinationOlStem` assignment introduced by P2-T1; + and none of the 4 is inside a `try` or `catch` block, verified by quoting the enclosing + statement of each. + +### Phase 5 — Change C, the recorded spec correction to the issue #439 assertion + +- [ ] [P5-T1] In `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, replace line 165 + ` router.SelectedFolderPath.Should().Be(fullTarget);` with + ` router.SelectedFolderPath.Should().Be(@"Clients\North");`. The expected value is + derived from the fixture in the same method: `archiveRoot` is `@"\Archive"` at `:123` and + `fullTarget` is `@"\aRcHiVe\Clients\North"` at `:124`, so `TryMakeArchiveRelative` matches through + the `OrdinalIgnoreCase` `StartsWith` at `ArchiveStemContract.cs:131`, the boundary character at + index 8 is a backslash per `:137-141`, and the stem is + `fullTarget.Substring(8).TrimStart('\\','/')`, which is `Clients\North`. Acceptance: the + fixed-string search + `rg -F -n 'router.SelectedFolderPath.Should().Be(@"Clients\North");' QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` + returns exactly 1 line and it is line 165, and the fixed-string search + `rg -F -n 'router.SelectedFolderPath.Should().Be(fullTarget);' QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` + returns 0 lines. +- [ ] [P5-T2] Rename the enclosing method at line 119 from + `Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch` to + `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively`, on one line so the file + line count is unchanged. Acceptance: + `rg -n "Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch" --glob "*.cs" .` + returns 0 lines, and + `rg -n "Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively" --glob "*.cs" .` + returns exactly 1 line. +- [ ] [P5-T3] Narrow the arrange comment at lines 121-122 to the provider claim it still supports, + keeping it exactly two lines so the file line count is unchanged. The replacement text is: + ` // Arrange: the presented target is rooted with casing different from the configured` + and + ` // root, so the provider must receive the original full path unchanged (#439).` + Acceptance: + `rg -n "so the provider must receive the original full path unchanged" QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` + returns exactly 1 line, and + `rg -n "already rooted with casing different" QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` + returns 0 lines. +- [ ] [P5-T4] Verify AC19: the companion provider assertion and `ToHierarchyPath` are preserved. Write + `evidence/regression-testing/p5-t4-provider-assertion-preserved.md`. Acceptance: lines 161 to 164 + of `BreadcrumbBridgeRouterIssue439Tests.cs` are byte-identical to their pre-change text, quoted + in the artifact; and, after `git add QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`, + `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` + produces no output at all, since this plan changes no line of that file. +- [ ] [P5-T5] Verify the file did not grow and that exactly one assertion changed. Run + `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs").Count'`, + then `git add QuickFiler.Test` followed by + `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- QuickFiler.Test`, and write + `evidence/regression-testing/p5-t5-single-assertion-change.md`. Acceptance: the line count is + exactly 694; among the diff's removed content lines, exactly one matches `.Should()`, and it is + ` router.SelectedFolderPath.Should().Be(fullTarget);`; and the artifact records the + change as a deliberate spec correction, stating that the issue #439 criterion that a rooted target + survives selection is superseded by issue #614's archive-relative-stem invariant, which #614 + enforced on the `SelectHierarchyPath` half and at the filing boundary but not on the `SelectRow` + half, and that this is explicitly not a weakened test; and the artifact additionally records the + P5-T2 rename (both the removed and the added method name) and the two replacement comment lines + from P5-T3, quoted verbatim, so that all three clauses of AC18 are evidenced in one artifact. +- [ ] [P5-T6] Run the issue #439 test class green. First re-run the P0-T13 analyzer build command + verbatim so that the Phase 5 test edits are compiled into + `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`; record its `EXIT_CODE:` and its + `(Rebuild target(s))` line in the same artifact. Without this rebuild the scoped run would + execute the assembly P4-T3 produced, which still carries the old method name and the old + assertion, and its acceptance would be unsatisfiable. Then use the P2-T7 command with the filter + `"/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue439Tests&TestCategory!=LiveOutlook"` + and the results directory `coverage\testresults\p5-t6`, and write + `evidence/regression-testing/p5-t6-issue439-green.md`. Acceptance: `EXIT_CODE: 0`; 0 failed; and + the artifact records that both `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` + and `Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection` passed. + +### Phase 6 — Change D, stale-comment cleanup + +- [ ] [P6-T1] Replace `QuickFiler/Controllers/EfcSelectionGuard.cs:30` with the fixed replacement text + given in "Fixed identifiers", item 1. The surrounding claim that the guard still rejects rooted + values stays as written; only the deferral wording changes. Acceptance: + `rg -n "is implemented by issue #637" QuickFiler/Controllers/EfcSelectionGuard.cs` returns + exactly 1 line, and `(Get-Content -LiteralPath "QuickFiler\Controllers\EfcSelectionGuard.cs").Count` + is exactly 79. +- [ ] [P6-T2] Replace `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146` with the fixed + replacement text given in "Fixed identifiers", item 2. Acceptance: + `rg -n "the producer normalizes" QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` returns + exactly 1 line, and it is line 146. +- [ ] [P6-T3] Replace the `because` string at `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:152` + with the fixed replacement text given in "Fixed identifiers", item 3, on one line so the file + line count is unchanged. Acceptance: + `rg -n "the producer now normalizes before this predicate is reached" QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` + returns exactly 1 line, and + `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs").Count` is + exactly 296. +- [ ] [P6-T4] Verify the deferral is gone and the guard's behavior is unchanged. First re-run the + P0-T13 analyzer build command verbatim so that the P6-T2 and P6-T3 test edits are compiled into + `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`; record its `EXIT_CODE:` and its + `(Rebuild target(s))` line in the same artifact. Without this rebuild the scoped run would + execute the assembly P4-T3 produced, which predates those edits, so the run would not be + evidence about the edited file that AC23 requires. Then run + `rg -c "deferred to issue #637" --glob "*.cs" .`, then run the P2-T7 command with the filter + `"/TestCaseFilter:FullyQualifiedName~EfcSelectionGuardTests&TestCategory!=LiveOutlook"` and the + results directory `coverage\testresults\p6-t4`, and write + `evidence/regression-testing/p6-t4-deferral-cleared.md`. The `*.cs` glob is load-bearing: the + phrase remains present in `spec.md`, in the research file, and in this plan, all of which are + Markdown and are correctly excluded. Acceptance: the `rg` invocation reports 0 matches and exits + non-zero, recorded with `ExpectedExitCode: 1` for that step; the scoped test run records + `EXIT_CODE: 0` with 0 failed; and the artifact records that + `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` passed and that neither + `IsValidFilingSelection` nor `IsValidCreationSelection` had any executable line changed, verified + by a `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- QuickFiler/Controllers/EfcSelectionGuard.cs` + run in the same task after `git add QuickFiler/Controllers/EfcSelectionGuard.cs`, whose only + changed line is line 30. +- [ ] [P6-T5] Commit changes A through D. Run + `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` + then + `git commit -m "fix(637): normalize the breadcrumb producer and the string filing overload"` and + write `evidence/other/p6-t5-commit.md`. A commit is required here because every Phase 7 and + Phase 8 gate is anchored to `ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD`, and an anchored diff + reports nothing for changes that are not yet committed. Acceptance: `EXIT_CODE: 0`; + `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and + `git diff --name-only ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- QuickFiler QuickFiler.Test` + lists exactly these eight paths and no others: + `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`, + `QuickFiler/Controllers/EfcDataModel.cs`, `QuickFiler/Controllers/EfcSelectionGuard.cs`, + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`, + `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, + `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs`, + `QuickFiler.Test/QuickFiler.Test.csproj`. + +### Phase 7 — Final QC toolchain loop and coverage delta + +Run the four steps in order. If any step fails or changes a file, return to P7-T1 and run the phase +again from the start. + +- [ ] [P7-T1] Format. Record + `git status --porcelain -- QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` + before the run, then run + `pwsh -NoProfile -Command 'dotnet tool run csharpier format .; "EXIT_CODE=$LASTEXITCODE"'`, then + record the same `git status --porcelain` invocation after the run, and write + `evidence/qa-gates/p7-t1-csharpier-format.md`. The exit code alone proves nothing here because + CSharpier exits 0 whether or not it rewrote a file, and its summary line was not observed in + this planning session, so no acceptance condition here reads it. Acceptance: `EXIT_CODE: 0`; both porcelain + outputs are recorded verbatim; and any path that differs between them is either a path this plan + changed or a path listed in the `BASELINE_FORMAT_DRIFT` section of + `evidence/baseline/p0-t12-csharpier-check.md`. A path that is in neither set means the repo-wide + format pass touched unrelated source and must be reported to the orchestrator before proceeding. +- [ ] [P7-T2] Verify the format. Run + `pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"'` and + write `evidence/qa-gates/p7-t2-csharpier-check.md`. Acceptance: `EXIT_CODE: 0`, and the captured + stdout is recorded verbatim. The exit code is the gate here rather than any summary wording, + because `check` is read-only and returns non-zero exactly when some file would be reformatted; + the write-mode discrimination that a read-only command cannot supply is provided by P7-T1's + before-and-after porcelain pair. Then, in this same task, run `git add QuickFiler QuickFiler.Test` + and `git commit -m "style(637): apply csharpier formatting before the coverage gates"`, so that + every subsequent `ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD` diff describes the same file + contents the P7-T5 build measured. If nothing changed, record that the commit was a no-op and + that the tree already matched `HEAD`. Record the commit result in + `evidence/qa-gates/p7-t2-csharpier-check.md`. +- [ ] [P7-T3] Analyzers. Run the P0-T13 command verbatim and write + `evidence/qa-gates/p7-t3-msbuild-analyzers.md`. Acceptance: `EXIT_CODE: 0`; the output contains + `(Rebuild target(s))`; the `Error(s)` count is 0; and the recorded `Command:` line contains + `/t:Rebuild` and `EnableNETAnalyzers=true` and `EnforceCodeStyleInBuild=true` and contains + neither `/t:Build` nor the solution-wide nullable opt-in property — record this as + `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. +- [ ] [P7-T4] Nullable. Run the P0-T14 command verbatim and write + `evidence/qa-gates/p7-t4-msbuild-nullable.md`. Acceptance: `EXIT_CODE: 0`; the output contains + `(Rebuild target(s))`; and the recorded `Command:` line contains `/t:Rebuild` and + `TreatWarningsAsErrors=true` and contains neither `/t:Build` nor the solution-wide nullable + opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in + the artifact. +- [ ] [P7-T5] Full test run with coverage. Run + `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-postchange.cobertura.xml` + and write `evidence/qa-gates/p7-t5-mstest-coverage.md`. The exit-code condition is stated + against the recorded baseline rather than as an unconditional zero, because the wrapper throws + whenever the inner vstest run reports any failure and a repository-wide zero-failure demand would + be unsatisfiable if the baseline itself carried failures. Acceptance: the number of discovered + test assemblies matches the number recorded in `evidence/baseline/p0-t15-mstest-coverage.md`; the + post-change failing set is a subset of the `BASELINE_FAILURE_SET` recorded there; no test that + passed in the baseline is failing now; the artifact names every baseline failure that is still + failing; and when `BASELINE_FAILURE_SET` is empty, `EXIT_CODE: 0` and 0 failed are required. + `Output Summary:` additionally carries the six numeric `/coverage` attribute values and the + derived line and branch percentages that P7-T6 reads, copied in once P7-T6 has produced them; + this task is not complete until that copy-back has been made, because the plan contract + requires the final-QC test-step artifact itself to carry the numeric coverage headline. +- [ ] [P7-T6] Read the post-change numeric coverage headline. Run the P0-T16 command with the input + path changed to `.\coverage\p7-t5-postchange.cobertura.xml` and write + `evidence/qa-gates/p7-t6-coverage-headline.md`. Acceptance: `EXIT_CODE: 0`, and `Output Summary:` + records all six numeric attribute values plus the derived line-coverage percentage and branch + percentage. +- [ ] [P7-T7] Verify changed-line coverage. Run the P0-T17 command with the input path changed to + `.\coverage\p7-t5-postchange.cobertura.xml`, and in the same task run + `git diff -U0 ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/EfcDataModel.cs` + to enumerate the added line numbers from the hunk headers. Re-derive the + `ToFilingStemOrVerbatim` line range against the post-format working tree in this same task, + recording the declaration line and the closing-brace line, and record both that range and the + range `evidence/regression-testing/p4-t2-helper-shape.md` recorded, stating whether they differ. + Every coverage assertion in this task is evaluated against the re-derived range; the P4-T2 range + is recorded for audit only. This re-derivation is required because P4-T2 measured the range in + Phase 4, P7-T1 then ran the write-mode formatter over `EfcDataModel.cs` — the first format pass + over the hand-written helper body, since Phases 2 through 6 contain no format step — and P7-T5 + measured the tree after it, so a formatter change to the helper's extent would make the P4-T2 + range identify uncovered lines in `OpenOlFolderAsync` or unrelated covered lines instead of the + helper. Write + `evidence/qa-gates/p7-t7-changed-line-coverage.md`. Acceptance: the artifact lists, per file, the + set of added line numbers and the set of line numbers with zero hits; the intersection of those + two sets is empty for both files; and, for `QuickFiler/Controllers/EfcDataModel.cs`, every line + number inside the re-derived `ToFilingStemOrVerbatim` range has non-zero hits, which is the + new-code coverage requirement for the new helper stated in AC29; and, for the re-derived + `ToFilingStemOrVerbatim` range, the artifact records the line nodes carrying `branch="True"` + together with their `condition-coverage` values, and states that the `IsFullOutlookPath` + conditional shows both branches taken — that is, a `condition-coverage` value of the form + `100% (2/2)` on that line — which is AC29's "both sides of its gate exercised" clause. The + capitalized `True` and the `condition-coverage` spelling are the forms the runner actually + emits, observed in + `docs/features/active/2026-08-28-quickfiler-keyboard-hook-leaks-to-outlook-677/evidence/qa-gates/coverage-final.cobertura.xml`, + where `branch="True"` occurs 11674 times and `branch="true"` occurs zero times; a lowercase + match must not be substituted. If no line node inside the re-derived range carries + `branch="True"` at all — the case where the compiler emits no separate branch point for the + guard — the artifact records that observation verbatim and satisfies AC29's clause instead by + naming the two fixed witness tests recorded green in + `evidence/regression-testing/p4-t4-helper-tests-green.md`: + `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` for the true side and + `ToFilingStemOrVerbatim_RelativeStem_ReturnsTheInputVerbatim` for the false side. The executor + does not choose between these two forms of proof: the branch-node form applies whenever a + `branch="True"` node exists in the re-derived range, and the witness-test form applies only when + none does. The `HEAD` this diff is anchored against already carries the formatting result, + because P7-T2 committed it before P7-T5 measured the tree. +- [ ] [P7-T8] Report the coverage delta. Write `evidence/qa-gates/p7-t8-coverage-delta.md` containing + three labelled numeric sections: baseline coverage, copied from + `evidence/baseline/p0-t16-coverage-headline.md`; post-change coverage, copied from + `evidence/qa-gates/p7-t6-coverage-headline.md`; and changed and new-code coverage, copied from + `evidence/qa-gates/p7-t7-changed-line-coverage.md`. Acceptance: all three sections carry numeric + values and none carries a placeholder; the post-change line-coverage percentage is at or above + 80, which is the floor the coverage runner itself enforces at + `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487-489` and which CLAUDE.md states, except + that if the baseline figure recorded in `evidence/baseline/p0-t16-coverage-headline.md` is + itself already below 80 the artifact records `BASELINE BELOW FLOOR`, reports that pre-existing + condition to the orchestrator, and the binding requirement becomes that the post-change figure is + at or above the recorded baseline figure; the + artifact records the post-change figure against the 85 percent line and 75 percent branch figures + in `.claude/rules/general-unit-test.md` and states explicitly which of the two repository-wide + figures each threshold comes from, without resolving the conflict; and the changed-line section + records an empty uncovered intersection. +- [ ] [P7-T9] File-size audit, run after the formatter rather than before it, because CSharpier can + change a file's line count. Run + `pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcSelectionGuard.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs","QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs","QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }'` + and write `evidence/qa-gates/p7-t9-file-sizes.md`. Acceptance: `EfcDataModel.cs` is at most 500; + `BreadcrumbBridgeRouterIssue637Tests.cs` is at most 500; `EfcDataModelIssue614Tests.cs` is at most + 500; `BreadcrumbBridgeRouterIssue439Tests.cs` is at most 694 and therefore has not grown, with + the exact value recorded; `EfcSelectionGuard.cs` is at most 79, with the exact value recorded; + `EfcSelectionGuardTests.cs` is at most 296, with the exact value recorded; and + `BreadcrumbBridgeRouter.Selection.cs` is at most 500. The three upper bounds replace exact + equalities because this task runs after a write-mode formatter that can reduce a line count, + and AC25 requires only that these files not grow. +- [ ] [P7-T10] Toolchain non-vacuity audit. Write `evidence/qa-gates/p7-t10-toolchain-audit.md` + enumerating the four final-QC command steps in order with their recorded `Command:` lines quoted + verbatim. Acceptance: the format step is `dotnet tool run csharpier format .` invoked through + `dotnet tool run` and not through a globally installed binary; both MSBuild `Command:` lines + contain `/t:Rebuild` and neither contains `/t:Build`; no evidence artifact of this feature + spells the solution-wide nullable opt-in token, verified by + `rg -n 'Nullable=enable' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**/*.md' --glob '!**/p7-t10-toolchain-audit.md'` + returning 0 matches and exiting non-zero, recorded with `ExpectedExitCode: 1` for that step; + and both MSBuild transcripts contain `(Rebuild target(s))`. Three scope restrictions on that + command are load-bearing. The directory operand must be this feature's folder rather than + `docs/features/active`, because 121 evidence Markdown files under other feature folders in that + tree contain the token and this plan cannot change them, so the parent-directory form can never + return 0 and the gate could never pass. The restriction to the evidence subtree is required + because `spec.md` and this plan both discuss the token in prose and both live at the root of + this feature's folder, so a scan of the folder without the `**/evidence/**/*.md` glob would + never return 0 either. The exclusion of `p7-t10-toolchain-audit.md` is required because this + artifact must record its own scan under `Command:`, and that command's pattern is the token + itself, so without the exclusion the gate would be defeated by the plan's own recording + instruction. Every other evidence artifact of this feature is in scope, and the "Nullable opt-in + token discipline" convention keeps each of them free of the token by recording + `NULLABLE_OPT_IN_PROPERTY: absent` instead. This artifact + additionally records the two deliberate substitutions against AC28's literal command list, each + with its justification: `msbuild` is invoked through the vswhere-resolved absolute path rather + than as a bare `msbuild` PATH entry, with the switch list character-for-character identical to + AC28's, and the artifact records the output of + `pwsh -NoProfile -Command 'if (Get-Command msbuild -ErrorAction SilentlyContinue) { "ON_PATH" } else { "NOT_ON_PATH" }'` + stating which of the two outcomes was observed and what it means. When the probe + records `NOT_ON_PATH`, the vswhere resolution is necessary and the artifact records it as such. + When the probe records `ON_PATH`, the artifact additionally records the output of the command + that prints the resolved source of the `msbuild` command — + `pwsh -NoProfile -Command '(Get-Command msbuild).Source'` — and states that the vswhere + resolution is used for determinism rather than necessity, because the PATH entry is not pinned + by this repository and can resolve to a different MSBuild, and records whether the two resolved + paths name the same binary; and the + test step is `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, whose inner + vstest call at `Invoke-MSTestWithCoverage.ps1:76` carries `/InIsolation` and + `/TestCaseFilter:TestCategory!=LiveOutlook` and whose coverage is collected by + `dotnet-coverage --output-format cobertura` rather than by `/EnableCodeCoverage`, which is the + repository's standard runner and the local analogue of + `.github/workflows/_mstest-coverage.yml:83`. The substitutions are recorded, not resolved. +- [ ] [P7-T11] Commit the QA evidence and any residual formatting result. Run + `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` + then + `git commit -m "chore(637): final QC toolchain pass and coverage evidence"` and write + `evidence/other/p7-t11-commit.md`. Acceptance: `EXIT_CODE: 0`, and + `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and + `git status --porcelain -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` + lists at most this task's own evidence + artifact and this plan file, with every other feature-folder path already in `HEAD`. Record + both outputs verbatim. This task's commit carries the Phase 7 evidence artifacts; the + formatting result itself is normally already in `HEAD` because P7-T2 committed it, so a + source-only no-op here is expected rather than exceptional. If the commit fails because nothing + changed at all, record that outcome and both `git status` results. + +### Phase 8 — Acceptance-criteria reconciliation + +Each task below verifies one acceptance criterion against evidence already on disk and then changes +that criterion's `- [ ]` to `- [x]` in the `## Acceptance Criteria` section of `spec.md`. No criterion +is checked off before its cited evidence exists. Exactly one criterion is checked off per task. + +- [ ] [P8-T1] AC1: cite `evidence/regression-testing/p3-t5-router-tests-green.md` showing + `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected` and + `RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection` passing, and + `evidence/regression-testing/p2-t7-router-tests-red.md` showing both failing before the fix. + Acceptance: both artifacts exist and name both tests; AC1 is checked off. +- [ ] [P8-T2] AC2: cite `evidence/regression-testing/p3-t5-router-tests-green.md` showing + `RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` and + `RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem` + passing. Acceptance: the artifact names both tests as passing; AC2 is checked off. +- [ ] [P8-T3] AC3: cite `evidence/regression-testing/p3-t2-nesting.md` for the nesting inside the + `IsFullOutlookPath` arm, and `evidence/regression-testing/p3-t5-router-tests-green.md` for + `RowSelected_RelativeFilingTarget_CommitsTheValueVerbatim` passing. Acceptance: both artifacts + exist and the nesting artifact quotes the edited body; AC3 is checked off. +- [ ] [P8-T4] AC4: cite `evidence/baseline/p1-t6-passthrough-tests.md` for the two existing tests, + `evidence/regression-testing/p3-t6-router-siblings.md` and + `evidence/regression-testing/p5-t6-issue439-green.md` for both passing unmodified, and + `evidence/regression-testing/p3-t5-router-tests-green.md` for the new + `RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim`. Acceptance: all three + artifacts exist and name the tests; AC4 is checked off. +- [ ] [P8-T5] AC5: cite `evidence/regression-testing/p3-t5-router-tests-green.md` for + `RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim`, and + `evidence/regression-testing/p4-t4-helper-tests-green.md` for + `ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim`. Acceptance: both artifacts exist + and name the tests; AC5 is checked off. +- [ ] [P8-T6] AC6: cite `evidence/regression-testing/p3-t2-nesting.md` for the preserved message + literal, `evidence/regression-testing/p3-t5-router-tests-green.md` for + `RowSelected_OutOfRootRootedTarget_IsStillRejected` and + `RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected`, and + `evidence/regression-testing/p3-t6-router-siblings.md` for + `RowSelected_OutOfRootFilingTarget_DoesNotStoreAFullOutlookPath`. Acceptance: all three artifacts + exist and name the tests; AC6 is checked off. +- [ ] [P8-T7] AC7: cite `evidence/regression-testing/p3-t2-nesting.md` recording that the new message + contains no `@`, and `evidence/regression-testing/p3-t5-router-tests-green.md` for + `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected`, which asserts through + `AssertRejectionDiagnosticWithoutIdentifiers`. Acceptance: both artifacts exist; AC7 is checked + off. +- [ ] [P8-T8] AC8: cite `evidence/regression-testing/p3-t3-selectionfile-diff.md` showing no hunk in + the original line range 109 to 139. Acceptance: the artifact exists and lists the hunk headers; + AC8 is checked off. +- [ ] [P8-T9] AC9: re-run both P1-T1 searches against the post-change tree and write + `evidence/qa-gates/p8-t9-selection-family-post.md`. Acceptance: both searches still return + exactly 9 lines with the same classification of 2 declarations and 7 call sites, and no Family-B + member appears in either result; AC9 is checked off. +- [ ] [P8-T10] AC10: cite `evidence/regression-testing/p3-t5-router-tests-green.md` for + `SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem`, which reaches + `SelectRow` through the public `SelectFirstRow` at `BreadcrumbBridgeRouter.cs:196-203` rather + than through the `rowSelected` inbound message. Acceptance: the artifact names that test as + passing; AC10 is checked off. +- [ ] [P8-T11] AC11: cite `evidence/regression-testing/p4-t2-helper-shape.md` for the single + `internal static` declaration, the single assignment call site, and the purity record, and + `evidence/regression-testing/p4-t4-helper-tests-green.md` for the eight tests that invoke the + helper directly without constructing an `EmailFiler`. Acceptance: both artifacts exist; AC11 is + checked off. +- [ ] [P8-T12] AC12: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for + `ToFilingStemOrVerbatim_RelativeStem_ReturnsTheInputVerbatim` and + `ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim`. Acceptance: the artifact names + both tests as passing; AC12 is checked off. +- [ ] [P8-T13] AC13: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for + `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` and + `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem`, together with + `evidence/regression-testing/p2-t8-helper-tests-red.md` showing both failing before the fix. + Acceptance: both artifacts exist and name both tests; AC13 is checked off. +- [ ] [P8-T14] AC14: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for + `ToFilingStemOrVerbatim_ArchiveRootExact_ReturnsTheInputVerbatimAndDoesNotThrow`, + `ToFilingStemOrVerbatim_OutOfRootRootedInput_ReturnsTheInputVerbatimAndDoesNotThrow`, + `ToFilingStemOrVerbatim_NullEmptyWhitespaceOrSeparatorOnlyAncestor_ReturnsTheInputVerbatim` and + `ToFilingStemOrVerbatim_NullOrEmptyCandidate_ReturnsTheInputVerbatim`, and + `evidence/regression-testing/p4-t2-helper-shape.md` for the record that the body contains no + `throw`. Acceptance: both artifacts exist and name all four tests; AC14 is checked off. +- [ ] [P8-T15] AC15: cite `evidence/baseline/p1-t5-toarchiverelativestem-tests.md` for the count of 8 + and the declaration line numbers, and + `evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md` for the zero-removed-line + diff and the 8 passing results including `ToArchiveRelativeStem_ArchiveRootItself_Throws`, and + `evidence/regression-testing/p4-t6-nongoals-untouched.md` for the unmodified `MAPIFolder` + overload and its call at the original line 345. Acceptance: all three artifacts exist; AC15 is + checked off. +- [ ] [P8-T16] AC16: re-run both P1-T2 searches against the post-change tree and write + `evidence/qa-gates/p8-t16-movetofolder-family-post.md`. Acceptance: the syntax-anchored search + still returns exactly 9 lines classified as 3 declarations and 6 call sites, the stem search + still returns 16 lines across 5 files, and no new overload and no signature change appears; AC16 + is checked off. +- [ ] [P8-T17] AC17: cite `evidence/regression-testing/p4-t6-nongoals-untouched.md`. Acceptance: the + artifact shows no hunk in the original ranges 299 to 334 and 336 to 386 and records that no + `Globals.Ol.ArchiveRootPath` read gained a `try` or `catch`; AC17 is checked off. +- [ ] [P8-T18] AC18: cite `evidence/regression-testing/p5-t5-single-assertion-change.md`, which + records all three clauses. Acceptance: the artifact records the corrected assertion at + line 165, the renamed method, and the narrowed two-line comment; AC18 is checked off. +- [ ] [P8-T19] AC19: cite `evidence/regression-testing/p5-t4-provider-assertion-preserved.md`. + Acceptance: the artifact quotes lines 161 to 164 byte-identically and shows no diff hunk over + `ToHierarchyPath` at the original `BreadcrumbBridgeRouter.cs:152-167`; AC19 is checked off. +- [ ] [P8-T20] AC20: cite `evidence/baseline/p1-t8-pinning-assertion.md` for the pre-change derivation + by two independent constructions, and + `evidence/regression-testing/p5-t5-single-assertion-change.md` for the post-change diff showing + exactly one removed `.Should()` line across the whole `QuickFiler.Test` tree. Acceptance: both + artifacts exist and agree that the count is 1; AC20 is checked off. +- [ ] [P8-T21] AC21: write `evidence/other/p8-t21-spec-correction-record.md` carrying the change + description text for this correction, copied from the record P5-T5 wrote into + `evidence/regression-testing/p5-t5-single-assertion-change.md`. This artifact is the designated + source text for the pull-request change description, so the statement is owned by this plan + rather than deferred to a step outside it. Acceptance: the artifact states that the issue #439 + criterion that a rooted target survives selection is superseded by issue #614's + archive-relative-stem invariant, which #614 enforced on the `SelectHierarchyPath` half and at the + filing boundary but not on the `SelectRow` half, and that the change is a deliberate spec + correction and explicitly not a weakened test; AC21 is checked off. +- [ ] [P8-T22] AC22: cite `evidence/baseline/p1-t4-deferral-records.md` for the pre-change count of 3 + and `evidence/regression-testing/p6-t4-deferral-cleared.md` for the post-change count of 0 over + `*.cs`. Acceptance: both artifacts exist and the post-change count is 0; AC22 is checked off. +- [ ] [P8-T23] AC23: cite `evidence/regression-testing/p6-t4-deferral-cleared.md` for the scoped + `EfcSelectionGuardTests` run with 0 failures including + `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary`, and for the single-line diff over + `EfcSelectionGuard.cs`. Acceptance: the artifact exists and records both; AC23 is checked off. +- [ ] [P8-T24] AC24: re-run both P1-T3 searches against the post-change tree and write + `evidence/qa-gates/p8-t24-selectedfolderpath-post.md`. Acceptance: the production surface is + still 9 lines across 3 files with 2 writes and 3 reads, no new write site appears, no new public + API member appears, and `rg -n "public string\? SelectedFolderPath \{ get; private set; \}" QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` + returns exactly 1 line; AC24 is checked off. +- [ ] [P8-T25] AC25: cite `evidence/qa-gates/p7-t9-file-sizes.md`. Acceptance: the artifact shows every + listed file at or under 500 lines, `BreadcrumbBridgeRouterIssue439Tests.cs` at or under 694 and + therefore not grown, and it records that the spec's stated 424 for `EfcDataModel.cs` was 423 on + the tree before the change; AC25 is checked off. The bound is stated as "at or under" rather + than "exactly" for the same reason it is in P7-T9: the figure is read after a write-mode + formatter that can reduce a line count, and AC25 requires only non-growth. +- [ ] [P8-T26] AC26: cite `evidence/regression-testing/p2-t9-compile-include-observed.md` for the + `Compile Include` line and the 10 observed test results, and + `evidence/regression-testing/p3-t5-router-tests-green.md` for the same 10 tests executing after + the fix. Acceptance: both artifacts exist and both record 10 executed tests; AC26 is checked off. +- [ ] [P8-T27] AC27: cite `evidence/qa-gates/p7-t4-msbuild-nullable.md` for the clean nullable build, + and verify in the same task that + `rg -n "^#nullable enable" QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` returns + line 1 and that `evidence/regression-testing/p3-t2-nesting.md` records that `stem` is a + non-nullable `string` passed to `CommitSelection` without a nullable temporary. Acceptance: all + three checks hold; AC27 is checked off. +- [ ] [P8-T28] AC28: cite `evidence/qa-gates/p7-t10-toolchain-audit.md`. Acceptance: the artifact + quotes all four final-QC commands verbatim in order, shows `/t:Rebuild` on both MSBuild lines, + records `NULLABLE_OPT_IN_PROPERTY: absent` for every one of the four quoted final-QC `Command:` + lines — do not spell the token in this task's own record — and shows every step recording + `EXIT_CODE: 0` in the final pass; and the artifact records the two deliberate substitutions + against AC28's literal command list, each with its justification: `msbuild` is invoked through + the vswhere-resolved absolute path rather than as a bare `msbuild` PATH entry, with the switch + list character-for-character identical to AC28's and with the recorded `ON_PATH` or + `NOT_ON_PATH` observation P7-T10 captured together with the statement of what that outcome + means: on `NOT_ON_PATH` the artifact must record the vswhere resolution as necessary, and on + `ON_PATH` it must record the resolved source of the `msbuild` command, state that the vswhere + resolution is used for determinism rather than necessity because the PATH entry is not pinned + by this repository, and record whether the two resolved paths name the same binary; and the + test step is + `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, whose inner vstest call at + `Invoke-MSTestWithCoverage.ps1:76` carries `/InIsolation` and + `/TestCaseFilter:TestCategory!=LiveOutlook` and whose coverage is collected by + `dotnet-coverage --output-format cobertura` rather than by `/EnableCodeCoverage`, which is the + repository's standard runner and the local analogue of + `.github/workflows/_mstest-coverage.yml:83`. The substitutions are recorded, not resolved. + AC28 is checked off. +- [ ] [P8-T29] AC29: cite `evidence/baseline/p0-t16-coverage-headline.md`, + `evidence/qa-gates/p7-t6-coverage-headline.md`, + `evidence/qa-gates/p7-t7-changed-line-coverage.md` and + `evidence/qa-gates/p7-t8-coverage-delta.md`. Acceptance: the baseline capture is under + `evidence/baseline/` and the post-change capture under `evidence/qa-gates/`, with no artifact + written to `evidence/coverage/` or to any path under `artifacts/`; the changed-line uncovered + intersection is empty; every line of the new helper has non-zero hits, judged against the + `ToFilingStemOrVerbatim` range P7-T7 re-derived against the post-format working tree rather than + against the pre-format range `evidence/regression-testing/p4-t2-helper-shape.md` recorded, with + P7-T7's record of whether the two ranges differ cited here; the artifact + `evidence/qa-gates/p7-t8-coverage-delta.md` records either a post-change line-coverage + percentage at or above 80 or an explicit `BASELINE BELOW FLOOR` finding with the post-change + figure at or above the recorded baseline; the `IsFullOutlookPath` conditional in the new helper + shows both branches taken, per the `condition-coverage` values P7-T7 recorded, or — when P7-T7 + records that the helper's range carries no `branch="True"` node — per the two witness tests + P7-T7 names; and AC29 is checked off. +- [ ] [P8-T30] AC30: verify no behavior outside changes A through D was altered. Run three commands in + this task, in this order. First the porcelain companion, + `git status --porcelain -- QuickFiler QuickFiler.Test UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. + Second + `git diff --name-only ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- QuickFiler QuickFiler.Test`. + Third + `git diff --name-only ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. + Write `evidence/qa-gates/p8-t30-scope-boundary.md` recording all three outputs verbatim. The + porcelain companion is required because a name-listing diff enumerates tracked changes only and + never reports an untracked path, so the two diffs alone cannot fail on a file this plan created + and left uncommitted. At the point this task runs the division of labour between the two + mechanisms is fixed and is stated here: P6-T5 committed changes A through D, P7-T2 committed the + formatting result, and P7-T11 committed the Phase 7 evidence, so both anchored diffs carry the + enumeration assertion, and the porcelain + span is expected to be empty because every path it covers is already in `HEAD`. That emptiness + is itself the assertion and not a null result — an untracked or unstaged file anywhere in those + nine trees, whether a new test file, a stray source file, or an evidence artifact written + outside the feature folder, appears in the porcelain output and in neither diff, so any line in + that output fails this task and must be reported to the orchestrator. The porcelain pathspec + omits `docs/features/active` deliberately: Phase 8 writes evidence artifacts and edits `spec.md` + under that path and P8-T33 commits them afterwards, so a porcelain span covering it would be + non-empty for reasons this gate is not measuring. Acceptance: the porcelain invocation produces + no output; the second command lists the eight paths enumerated in P6-T5, plus — only when the + `BASELINE_FORMAT_DRIFT` section of `evidence/baseline/p0-t12-csharpier-check.md` is non-empty — + the paths in that section that lie under `QuickFiler` or `QuickFiler.Test`, each of which the + artifact must show as a formatting-only change committed by P7-T2, and no others; when + `BASELINE_FORMAT_DRIFT` is empty the list is exactly the eight paths; the third command + produces no output, which is the evidence that `UtilitiesCS`, `TaskMaster`, + `ToDoModel`, `Tags`, `TaskVisualization`, `UtilitiesCS.Test` and `TaskMaster.Test` contain no + changed file; and AC30 is checked off. +- [ ] [P8-T31] Verify all thirty criteria are checked off, using two independently constructed + section-scoped counts. Construction 1, range-scoped: extract the lines of `spec.md` between the + line matching `^## Acceptance Criteria$` and the line matching `^## Risks & Mitigations$`, and + count within that slice the lines matching `^- \[x\] AC` and the lines matching `^- \[ \] AC`. + Construction 2, token-scoped and independent of the range extraction: count over the whole file + the lines matching `^- \[x\] AC` and the lines matching `^- \[ \] AC`. Construction 2 is + section-discriminating by token rather than by range, because the five checkboxes outside the + section at `spec.md:54`, `:55`, `:56`, `:57` and `:86` are not followed by `AC`. Write + `evidence/qa-gates/p8-t31-ac-reconciliation.md`. Acceptance: both constructions report 30 checked + and 0 unchecked; both agree; and the artifact records that an unscoped count of every `- [x]` and + `- [ ]` line in `spec.md` would over-report by exactly 5, naming those five line numbers. +- [ ] [P8-T32] Record the spec-versus-tree discrepancies found during this work in + `evidence/other/p8-t32-spec-tree-discrepancies.md`. Acceptance: the artifact records the three + items listed under "Tree observations recorded while authoring this plan" that concern `spec.md` + citations — the 423-versus-424 line count for `EfcDataModel.cs`, the `:167-214`-versus-`:167-213` + span of `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary`, and the + `:143-147`-versus-`:143-146` span of the clear-on-rebind block — and states for each that no + acceptance criterion's binding clause is affected. `spec.md` itself is not edited for these; only + the acceptance-criteria checkboxes are edited by this phase. +- [ ] [P8-T33] Final commit and clean tree. Run + `git add docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` + then + `git commit -m "docs(637): reconcile acceptance criteria and record final evidence"` and write + `evidence/other/p8-t33-final-commit.md`. Acceptance: `EXIT_CODE: 0`, and + `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and + `git status --porcelain -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` + lists at most this task's own evidence + artifact and this plan file, with every other feature-folder path already in `HEAD`. Record + both outputs verbatim. The pathspec scoping is required because `.claude/` is tracked and + carries unrelated in-flight modifications that this plan must not commit, and because sibling + feature folders under `docs/features/active` are owned by other work. No sibling folder there is + untracked at planning time, but this task runs long after planning, and a concurrent run in this + checkout can leave an untracked or modified sibling folder under that parent directory before + this task executes; a `git add` over the parent directory would then commit another feature's + work onto this branch, and a `git status --porcelain` span over the parent directory would report + that folder and make this gate unsatisfiable. Both spans are therefore scoped so that this gate + cannot depend on state this plan does not own. The feature-folder + span is stated separately because this task necessarily writes its own artifact after the + commit and checks off its own box in this plan file, both of which live under this feature's + folder. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/research/research.2026-08-29T12-30.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/research/research.2026-08-29T12-30.md new file mode 100644 index 000000000..4601bce86 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/research/research.2026-08-29T12-30.md @@ -0,0 +1,753 @@ +# Research — Issue #637: `BreadcrumbBridgeRouter.SelectRow` emits a rooted path, leaving #614 D1 half-closed + +- Timestamp: 2026-08-29T12-30 +- Branch: `bug/breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` +- Base: `origin/main` at `ecdb1c84ba8541ab67042985919cfed4df768c01` +- All paths below are relative to the isolated worktree root `/.claude/worktrees/`. +- Every line number was read against the current tree on this branch. No citation is carried forward from any other document. + +--- + +## 0. Executive summary of verified findings + +1. The defect is real and reproducible by inspection. `SelectRow` rejects only an out-of-root rooted target; a rooted target **at or under** the bound archive root is committed verbatim (`QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs:96-106`). +2. There are **two unrelated `SelectRow` families** in this repository. Only the `BreadcrumbBridgeRouter` family (private, `BreadcrumbRow` parameter) is in scope. The `SelectRow(int index)` family on `BreadcrumbStateModel` / `BreadcrumbSelectionSession` / `FolderBreadcrumbBridgeRouter` / `BreadcrumbBridgeCoordinator` is a different surface and is not touched by this fix. A single-pass grep on the bare identifier conflates the two and yields a count roughly ten times too large. +3. **A blanket normalization in `SelectRow` would be a regression.** `SelectRow` also carries the trash pseudo-row text and ordinary *relative* suggestion targets. `TryMakeArchiveRelative` returns `false` for both, so an unconditional "commit only when `TryMakeArchiveRelative` succeeds" rewrite would reject every normal selection. The change must remain scoped inside the existing `ArchiveStemContract.IsFullOutlookPath(selection)` arm. +4. **The issue's third finding is materially inaccurate against the current tree.** `ButtonOK_Click` is `async void`, but it delegates to `ButtonOkClickAsync`, which wraps the whole chain in `try { ... } catch (System.Exception ex) { BoundaryErrorSink(ex.Message, ex); }` (`QuickFiler/Controllers/EfcFormController.cs:460-475`). An `InvalidOperationException` from `Globals.Ol.ArchiveRootPath` is therefore **logged, not unhandled**, on the OK-button path. The genuine defect on that path is different and is described in §10. +5. The keyboard entry points to the same chain (`KbdExecuteAsync(ActionOkAsync)` and the always-on `Keys.Return` action) have **no** try/catch, so they are a separate and narrower exposure than the button path. + +--- + +## 1. `BreadcrumbBridgeRouter.SelectRow` — current body and the surviving branch + +File: `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` + +```csharp + 83 private void SelectRow(BreadcrumbRow row) + 84 { + 85 if (row.Kind == BreadcrumbRowKind.Banner) + 86 { + 87 return; // Banner rows are never selectable. + 88 } + 89 + 90 string selection = + 91 row.Kind == BreadcrumbRowKind.TrashPseudoRow + 92 ? BreadcrumbRowBuilder.TrashRowText + 93 : row.FilingTarget; + 94 // #614 D2: reject only an out-of-root FULL Outlook target; a rooted target at or + 95 // under the root passes verbatim (#439) and no bound root leaves the row unguarded. + 96 if ( + 97 _boundRoot.Length != 0 + 98 && ArchiveStemContract.IsFullOutlookPath(selection) + 99 && !ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out _) +100 ) +101 { +102 log.Error("Breadcrumb row rejected: target is outside the archive root."); +103 return; +104 } +105 +106 CommitSelection(row, selection); +107 } +``` + +`CommitSelection` is the sole non-clearing write site: + +```csharp +131 private void CommitSelection(BreadcrumbRow row, string selection) +132 { +133 _selectedRowId = row.RowId; +134 SelectedFolderPath = selection; +135 PostOutbound( +136 new BreadcrumbRenderMessage(_renderer.RenderRows(_rows, _selectedRowId), null) +137 ); +138 SelectedFolderPathChanged?.Invoke(this, SelectedFolderPath); +139 } +``` + +### How a rooted at-or-under-root value survives + +The guard at lines 96-100 is a three-term conjunction whose **third term is negated**. `TryMakeArchiveRelative` returns `true` for a path at or under the root and discards the stem into `out _` (line 99). Therefore: + +| `selection` shape | `_boundRoot.Length != 0` | `IsFullOutlookPath` | `TryMakeArchiveRelative` | `!Try...` | Guard fires? | Committed value | +|---|---|---|---|---|---|---| +| Rooted, strictly under root | true | true | **true** | false | **no** | rooted value, verbatim | +| Rooted, exactly equal to root | true | true | **true** (stem empty) | false | **no** | rooted root, verbatim | +| Rooted, out of root / cross-store | true | true | false | true | yes | nothing (rejected) | +| Relative stem (`Clients\North`) | true | **false** | n/a (short-circuited) | n/a | no | relative value, verbatim (correct) | +| `Trash to Delete` pseudo-row | true | **false** | n/a | n/a | no | `Trash to Delete` (correct) | +| Any value, no bound root | **false** | n/a | n/a | n/a | no | verbatim (deliberate #439 pass-through mode) | + +The branch that must change is the **fall-through at line 106 for rows 1 and 2 of that table**: rows where `IsFullOutlookPath(selection)` is true and `TryMakeArchiveRelative` succeeds. The out-parameter is currently discarded (`out _`, line 99); binding it and committing the stem is the mechanically minimal change, plus a deterministic non-selection when that stem is empty. + +**Critical constraint for the planner:** the guard's `IsFullOutlookPath` short-circuit is load-bearing. `TryMakeArchiveRelative("Clients\\North", @"\\mailbox@example.com\Archive", out _)` returns **false** (the prefix test at `ArchiveStemContract.cs:129-135` fails), and `TryMakeArchiveRelative("Trash to Delete", root, out _)` also returns **false**. A rewrite of the form "commit only when `TryMakeArchiveRelative` succeeds with a non-empty stem" applied to the *whole* method would therefore reject every ordinary suggestion row and the trash pseudo-row. The new behavior must be nested inside the `IsFullOutlookPath` arm. + +--- + +## 2. `SelectHierarchyPath` — the concrete target semantics + +File: `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` + +```csharp +109 private void SelectHierarchyPath(BreadcrumbRow row, string fullPath) +110 { +111 if (_boundRoot.Length == 0) +112 { +113 CommitSelection(row, fullPath); // Preserved no-archive-root binding mode. +114 return; +115 } +116 +117 // #614 D1/D9: a path outside the archive root, and the root itself, are deterministic +118 // non-selections; the prior selection stays unchanged and is never nulled (#499). +119 if ( +120 !ArchiveStemContract.TryMakeArchiveRelative(fullPath, _boundRoot, out string stem) +121 || stem.Length == 0 +122 ) +123 { +124 log.Error("Breadcrumb selection rejected: not a folder inside the archive root."); +125 return; +126 } +127 +128 CommitSelection(row, stem); +129 } +``` + +Stated concretely rather than by reference, the four behaviors the planner should mirror in `SelectRow`'s rooted arm are: + +- **B1 (no bound root):** `_boundRoot.Length == 0` short-circuits to a verbatim commit (line 111-115). `SelectRow` already has the equivalent short-circuit as the first conjunct at line 97. +- **B2 (empty stem is a non-selection):** the disjunct `|| stem.Length == 0` at line 121 is what converts the archive-root-exact case — for which `TryMakeArchiveRelative` returns `true` — into a rejection. This is the specific clause `SelectRow` lacks. +- **B3 (rejection is an early `return`, not a null-out):** the method returns without touching `SelectedFolderPath`, so a prior valid selection survives. This is the #499 interaction; `CommitSelection` is never reached, so no `SelectedFolderPathChanged` event is raised. +- **B4 (rejection is diagnosed value-free):** `log.Error` at line 124 with a fixed message that embeds neither the path nor the root. The message text is `"Breadcrumb selection rejected: not a folder inside the archive root."`. `SelectRow`'s existing rejection message at line 102 is `"Breadcrumb row rejected: target is outside the archive root."` — also value-free. Any new rejection message must stay value-free; `BreadcrumbBridgeRouterIssue614Tests.AssertRejectionDiagnosticWithoutIdentifiers` (`QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs:310-326`) asserts that no message containing the fragment contains `@`. +- **B5 (commit the stem):** `CommitSelection(row, stem)` at line 128 — the stem, not the input. + +--- + +## 3. `TryMakeArchiveRelative` — signature, out-parameter semantics, truth table + +File: `UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs` + +Signature (`:106-110`): + +```csharp +public static bool TryMakeArchiveRelative( + string fullPath, + string archiveRoot, + out string stem +) +``` + +Body (`:112-145`), abbreviated with line numbers: + +- `:112` `stem = string.Empty;` — the out-parameter is **unconditionally initialized to empty first**, so `stem` is never the input on any exit path. +- `:113-116` returns `false` when `fullPath` is null/empty or `archiveRoot` is null/empty/whitespace. +- `:118` `root = archiveRoot.TrimEnd('\\', '/')`; `:119-122` returns `false` when the trimmed root is empty (separator-only root). +- `:124-127` `if (string.Equals(fullPath, root, OrdinalIgnoreCase)) return true;` — **returns `true` with `stem` still `string.Empty`**. +- `:129-135` returns `false` when `fullPath.Length <= root.Length` or the ordinal-case-insensitive `StartsWith` fails. +- `:137-141` returns `false` when the character at `root.Length` is not a separator (the `Archive2` near-miss guard). +- `:143-144` `stem = fullPath.Substring(root.Length).TrimStart('\\', '/'); return true;` + +### Full truth table + +| # | Case | Example (`fullPath`, `archiveRoot`) | Returns | `stem` | Evidence | +|---|---|---|---|---|---| +| a | Rooted strictly under root | `\Archive\Clients\North`, `\Archive` | `true` | `Clients\North` | `:143-144` | +| a' | Rooted under root, differing case | `\aRcHiVe\Clients\North`, `\Archive` | `true` | `Clients\North` | `:131` `OrdinalIgnoreCase` | +| a'' | Root has trailing separator | `\Archive\Clients`, `\Archive\` | `true` | `Clients` | `:118` `TrimEnd` | +| a''' | Forward-slash boundary | `\Archive/Clients`, `\Archive` | `true` | `Clients` | `:138` accepts `/` | +| b | Rooted exactly equal to root | `\Archive`, `\Archive` | **`true`** | **`string.Empty`** | `:124-127` | +| c | Rooted outside root / different store | `\\other@example.org\Archive\Clients`, `\\mailbox@example.com\Archive` | `false` | `string.Empty` | `:131` | +| c' | Separator-boundary near miss | `\Archive2\Clients`, `\Archive` | `false` | `string.Empty` | `:137-141` | +| d | Already-relative value | `Clients\North`, `\\mailbox@example.com\Archive` | **`false`** | `string.Empty` | `:130-131` (`StartsWith` fails) | +| e | Null or empty `fullPath` | `null` or `""`, any root | `false` | `string.Empty` | `:113-116` | +| f | Empty / whitespace / separator-only `archiveRoot` | any, `""` / `" "` / `"\"` | `false` | `string.Empty` | `:113-116`, `:118-122` | + +Case (b) returning `true` is the exact mechanism by which `SelectRow`'s negated third conjunct lets the archive-root-exact value through. Case (d) returning `false` is the exact reason a blanket rewrite is unsafe (§1). + +Behaviors (a), (a'), (a''), (a'''), (b), (c), (c'), (e), (f) are each pinned by a named test in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemContractTests.cs` (test methods at `:164, :179, :194, :209, :224, :239, :254, :269, :284, :299, :315`). Case (d) is not separately named there but is the same code path as (c). + +--- + +## 4. `ArchiveStemContract` — complete public surface + +File: `UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs`. The type is `public static class ArchiveStemContract` (`:18`) in namespace `UtilitiesCS.OutlookObjects.Folder`, carrying `#nullable enable` at `:1`. It has exactly **three** public members and two private constants (`BackslashSeparator` `:20`, `ForwardSeparator` `:21`). + +1. `public static bool IsFullOutlookPath(string value)` — `:41`. Returns `false` for null/empty (`:43-46`); `true` when the first character is `\` or `/` (`:48-53`); `true` when `value.Length > 1 && value[1] == ':'` (`:55`); otherwise `false`. + +2. `public static void RequireArchiveRelativeStem(string value, string paramName)` — `:68`. Two validations, both throwing `ArgumentException` with `paramName` as the second constructor argument: + - `:70-77` when `string.IsNullOrWhiteSpace(value)`, message is `paramName` concatenated with: + `" must be a non-empty archive-relative path (relative to the Outlook archive root); it was null, empty, or whitespace."` + - `:79-86` when `IsFullOutlookPath(value)`, message is `paramName` concatenated with: + `" must be an archive-relative path (relative to the Outlook archive root), but a full (rooted) Outlook or filesystem path was supplied. The value is withheld from this message because it can contain a mailbox address or user-profile path."` + - Neither message embeds `value`. Both are exercised by `ArchiveStemContractTests` at `:80, :95, :106, :117, :133, :150`. + +3. `public static bool TryMakeArchiveRelative(string fullPath, string archiveRoot, out string stem)` — `:106`. See §3. + +`RequireArchiveRelativeStem` is invoked at exactly three production sites: +- `UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFilerConfig.cs:189-192` (`ResolvePaths(Folder)`) +- `UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFilerConfig.cs:210-213` (`ResolvePaths()`) +- `QuickFiler/Controllers/EfcDataModel.cs:384` (inside `ToArchiveRelativeStem`) + +--- + +## 5. Call-site census — the selection family (EXHAUSTIVE, two independent searches) + +### The two-family disambiguation + +`SelectRow` is an overloaded name across two unrelated surfaces: + +- **Family A (in scope):** `BreadcrumbBridgeRouter.SelectRow(BreadcrumbRow row)` and `BreadcrumbBridgeRouter.SelectHierarchyPath(BreadcrumbRow row, string fullPath)` — both `private`, both declared in `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`. +- **Family B (NOT in scope):** `SelectRow(int index)` on `UtilitiesCS.OutlookObjects.Folder.BreadcrumbStateModel` (`BreadcrumbStateModel.cs:120`), `BreadcrumbSelectionSession` (`BreadcrumbSelectionSession.cs:176`), `FolderBreadcrumbBridgeRouter` (`FolderBreadcrumbBridgeRouter.cs:178`), and `QuickFiler.Viewers.BreadcrumbBridgeCoordinator` (`BreadcrumbBridgeCoordinator.cs:125`). Family B has no `SelectedFolderPath`, no `_boundRoot`, and no `ArchiveStemContract` reference; it is the ItemViewer drop-down selector surface. + +Any count that mixes the families is wrong. A single-pass grep on the bare token `SelectRow` returns **106 matching lines across 34 files** repository-wide; only **6** of those lines belong to Family A. + +### Search 1 + +Tool: `Grep`, pattern `SelectRow|SelectHierarchyPath`, glob `*.cs`, repository root, `output_mode: content`. +Result: 106 matching lines across 34 files. Family-A lines isolated from that output: 12 (6 in `BreadcrumbBridgeRouter*.cs`, plus 6 lines in test/doc text that merely *name* the methods without calling them — `EfcSelectionGuard.cs:30`, `BreadcrumbBridgeRouterQueueTests.Part2.cs:252/265/275/314`, `BreadcrumbBridgeCoordinatorTests.cs:432`). + +### Search 2 (independently constructed) + +Tool: `Grep`, pattern `Select(Row|HierarchyPath)\s*\(` (call/declaration syntax only, excluding prose and identifier-substring matches), path scoped to the `QuickFiler` production project. +Result: **12 matching lines across 4 files**, of which 3 lines (`BreadcrumbBridgeCoordinator.cs:125`, `:127`, `ItemViewer.FolderSearch.cs:27`) are Family B and 9 lines are Family A. + +### Agreement + +Both searches independently produce the **same 9 Family-A syntax lines**: 2 declarations + 7 call sites. Counts agree. + +### The census + +**Declarations — 2. There is no interface declaration and no overload of either method anywhere in the repository.** Both are `private` instance methods on the `sealed partial class BreadcrumbBridgeRouter`; `QuickFiler/Interfaces/` contains no member of either name. + +| # | Kind | File:line | Signature | +|---|---|---|---| +| D1 | declaration | `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs:83` | `private void SelectRow(BreadcrumbRow row)` | +| D2 | declaration | `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs:109` | `private void SelectHierarchyPath(BreadcrumbRow row, string fullPath)` | + +**Call sites — 7 (4 to `SelectRow`, 3 to `SelectHierarchyPath`). All 7 are in production; zero test call sites, because both members are private and every test drives them through `ProcessInboundAsync` or the public `SelectFirstRow`.** + +| # | Target | File:line | Calling context | Behavior change from this fix? | +|---|---|---|---|---| +| C1 | `SelectRow` | `BreadcrumbBridgeRouter.cs:201` | `SelectFirstRow()` (public; called from `EfcFormController.cs:438`) | **Yes**, when row 0's `FilingTarget` is rooted at-or-under the bound root | +| C2 | `SelectRow` | `BreadcrumbBridgeRouter.cs:286` | `ProcessInboundAsync`, `BreadcrumbMessageTypes.RowSelected` arm | **Yes**, same condition | +| C3 | `SelectRow` | `BreadcrumbBridgeRouter.Arrows.cs:153` | `HandleUpArrow` (Up arrow onto a previous row) | **Yes**, same condition | +| C4 | `SelectRow` | `BreadcrumbBridgeRouter.Arrows.cs:161` | `MoveSelection(row, step)` (Down arrow) | **Yes**, same condition | +| C5 | `SelectHierarchyPath` | `BreadcrumbBridgeRouter.Selection.cs:33` | `ActivateSegment` (segmentActivate message) | No — method not modified | +| C6 | `SelectHierarchyPath` | `BreadcrumbBridgeRouter.Selection.cs:47` | `ActivateChild` (renderedChildActivate message) | No | +| C7 | `SelectHierarchyPath` | `BreadcrumbBridgeRouter.Arrows.cs:138` | `TryRightTreeTransitionAsync` (#440 Right descent) | No | + +**Delegate / event wiring reaching Family A — 2 indirect entry points, neither a direct call site:** +- `BreadcrumbBridgeRouter.cs:55` `_host.MessageReceived += OnHostMessageReceived;` → `OnHostMessageReceived` (`:291`) → `ProcessInboundAsync` → C2 / C5 / C6 / C7. +- `EfcFormController.cs:438` `_router?.SelectFirstRow();` → C1. + +All four `SelectRow` call sites (C1-C4) share one implementation, so the behavior change is uniform across them; there is no per-call-site divergence to reason about. + +--- + +## 6. `MoveToFolderAsync` overload census (EXHAUSTIVE, two independent searches) + +### Search 1 + +Tool: `Grep`, pattern `MoveToFolderAsync`, glob `*.cs`, repository root. +Result: **16 matching lines across 6 files.** + +### Search 2 (independently constructed — broader stem, to catch any non-`Async` sibling or partially-renamed member the first pattern would miss) + +Tool: `Grep`, pattern `MoveToFolder`, glob `*.cs`, repository root. +Result: **16 matching lines across 6 files** — byte-identical line set. + +### Agreement + +Counts agree at 16. The broader stem finds **no** `MoveToFolder` member without the `Async` suffix, confirming there is no non-async sibling and no partially-renamed overload. + +### Declarations — 3 across 2 declaring types. There is no interface declaring any member of this family. + +| # | Declaring type | File:line | Full signature | +|---|---|---|---| +| M1 | `QuickFiler.Controllers.EfcDataModel` | `QuickFiler/Controllers/EfcDataModel.cs:259-265` | `async public Task MoveToFolderAsync(string folderpath, bool saveAttachments, bool saveEmail, bool savePictures, bool moveConversation)` | +| M2 | `QuickFiler.Controllers.EfcDataModel` | `QuickFiler/Controllers/EfcDataModel.cs:336-343` | `public async Task MoveToFolderAsync(MAPIFolder folder, string olAncestor, bool saveAttachments, bool saveEmail, bool savePictures, bool moveConversation)` | +| M3 | `QuickFiler.EfcHomeController` | `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:89-95` | `internal Task MoveToFolderAsync(string selectedFolder, bool saveAttachments, bool saveEmail, bool savePictures, bool moveConversation)` | + +M3 is a **separate type's same-named forwarder**, not an overload of M1/M2. It is a test seam: it forwards to `_dataModel.MoveToFolderAsync` (M1) unless the injectable `MoveToFolderAsyncAction` delegate property (`EfcHomeController.ExecuteMoves.cs:14-21`) is set. A census that counted only "overloads of `EfcDataModel.MoveToFolderAsync`" would report 2 and miss M3 entirely. + +### Call sites — 5 production + 1 test = 6 + +| # | File:line | Resolves to | Notes | +|---|---|---|---| +| K1 | `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:78-84` | M3 | inside `ExecuteMovesCoreAsync` | +| K2 | `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:98-104` | M1 | the `MoveToFolderAsyncAction is null` branch of M3 | +| K3 | `QuickFiler/Controllers/EfcDataModel.cs:346-352` | M1 | inside M2; the only caller of `ToArchiveRelativeStem` | +| K4 | `QuickFiler/Controllers/EfcFormController.cs:537-544` | M2 | `ButtonCreateClickAsync`, after `CreateFolderAsync` returns a `MAPIFolder` | +| K5 | `QuickFiler/Controllers/EfcFormController.cs:843-852` | M2 | `CreateFolderAsync` (the keyboard 'N' path) | +| K6 | `QuickFiler.Test/Controllers/EfcHomeControllerExecuteMovesTests.cs:87-93` | M3 | `MoveToFolderAsync_WithInjectedAction_UsesCapturedMoveOptions` | + +Plus 3 non-call textual references: a delegate-property declaration (`ExecuteMoves.cs:21`), a null test on that property (`ExecuteMoves.cs:97`), a delegate invocation (`ExecuteMoves.cs:105`), two test assignments to the property (`EfcHomeControllerExecuteMovesTests.cs:69, :125`), one test method name (`:65`), and one comment (`EfcHomeControllerTests.cs:55`). 3 declarations + 6 call sites + 7 other = 16 lines. The line accounting closes. + +### The `string` overload's `DestinationOlStem` assignment + +`QuickFiler/Controllers/EfcDataModel.cs`: + +```csharp +282 var config = new EmailFilerConfig() +283 { +284 SaveMsg = saveEmail, +285 SaveAttachments = attachments, +286 SavePictures = savePictures, +287 DestinationOlStem = folderpath, +288 Globals = Globals, +289 OlAncestor = Globals.Ol.ArchiveRootPath, +290 FsAncestorEquivalent = folderRoot, +291 }; +``` + +Line 287 assigns `folderpath` verbatim; there is no `ToArchiveRelativeStem` call and no `ArchiveStemContract` call anywhere in M1. The same verbatim-assignment shape recurs in two sibling methods on the same type that are **not** part of the `MoveToFolderAsync` family but share the identical exposure: `OpenOlFolderAsync` (`:308`) and `OpenFsFolderAsync` (`:326`), both of which also read `Globals.Ol.ArchiveRootPath` (`:310`, `:328`). + +### The `MAPIFolder` overload's normalization + +```csharp +336 public async Task MoveToFolderAsync( +337 MAPIFolder folder, +338 string olAncestor, +... +345 var folderpath = ToArchiveRelativeStem(folder.FolderPath, olAncestor); +346 var result = await MoveToFolderAsync( +347 folderpath, +... +353 if (!result) +354 { +355 MessageBox.Show($"Cannot move to folderpath {folderpath}"); +356 } +357 } +``` + +`ToArchiveRelativeStem` is declared at `:372-386` and is called from **exactly one** site (`:345`), confirming the issue's statement. + +### Can the two overloads converge on one normalization path? + +They are already partially converged: M2 normalizes and then delegates to M1 (`:345-352`), so M1 is the single funnel. Two facts block moving normalization *into* M1 as a straight lift: + +1. **Parameter asymmetry.** M2 receives `olAncestor` as an explicit parameter (`:338`) supplied by the caller (`EfcFormController.cs:539` and `:846`, both `_globals.Ol.ArchiveRootPath`). M1 has no ancestor parameter; it reads `Globals.Ol.ArchiveRootPath` internally at `:289`. Normalizing inside M1 would require reading that property *before* deciding whether normalization applies, which widens rather than narrows the `InvalidOperationException` exposure described in §10. +2. **Input-domain asymmetry.** M2's input is always a full Outlook `MAPIFolder.FolderPath`, so `ToArchiveRelativeStem`'s unconditional throw is correct there. M1's input is a *presented selection* that is normally already relative and may legitimately be the `"Trash to Delete"` sentinel (M1 branches on it at `:272`). An unconditional `ToArchiveRelativeStem` in M1 would throw on every ordinary relative stem and on the trash sentinel. Convergence would require the same `IsFullOutlookPath`-gated shape as §1, i.e. normalize-if-rooted rather than always-normalize. + +A third, non-blocking observation: M1's `folderpath` is also compared by value against the literal `"Trash to Delete"` at `:272` (`bool attachments = (folderpath != "Trash to Delete") ? saveAttachments : false;`). Any normalization inserted upstream of that comparison must leave the trash sentinel untouched or the attachment-save behavior silently flips. + +--- + +## 7. `SelectedFolderPath` consumer census (EXHAUSTIVE, two independent searches) + +### Search 1 + +Tool: `Grep`, pattern `SelectedFolderPath`, glob `*.cs`, repository root, `output_mode: content`. +Result: **74 matching lines across 9 files.** + +### Search 2 (independently constructed — differencing two counts to separate the property from the same-prefixed event, which a single content grep visually conflates) + +Tool: `Grep`, pattern `SelectedFolderPath`, `output_mode: count` → 74 lines / 9 files. +Tool: `Grep`, pattern `SelectedFolderPathChanged`, `output_mode: count` → 13 lines / 5 files. +Derived: 74 − 13 = **61 lines that reference the property without the event**, plus 2 lines (`BreadcrumbBridgeRouter.Selection.cs:138`, `BreadcrumbBridgeRouter.cs:146`) that carry both tokens on one line. + +### Agreement + +Both searches place the entire population in **9 files: 2 production, 7 test**, and both place **all** production occurrences in exactly **3 files**. Counts agree. + +### Production surface — complete + +| # | File:line | Kind | Detail | +|---|---|---|---| +| P1 | `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:59` | declaration | `public string? SelectedFolderPath { get; private set; }` — the setter is `private`, so no external write is possible | +| P2 | `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:61` | doc reference | `` on the event | +| P3 | `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:143` | **read** | `if (SelectedFolderPath != null)` — the #499 change-detection guard in `BindRowsAsync` | +| P4 | `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:145` | **write** | `SelectedFolderPath = null;` — the #499 clear-on-rebind | +| P5 | `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs:134` | **write** | `SelectedFolderPath = selection;` in `CommitSelection` | +| P6 | `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs:138` | **read** | event payload argument | +| P7 | `QuickFiler/Controllers/EfcFormController.cs:321` | **read** | `get => _router?.SelectedFolderPath;` — the sole cross-type read | + +**Total: 2 writes (P4, P5), 3 reads (P3, P6, P7), 1 declaration, 1 doc reference.** P5 is the only write reached by `SelectRow`. + +### Downstream blast radius through P7 + +`EfcFormController.SelectedFolder` (`:316-322`) is the single external consumer. Its own readers, exhaustively: + +| Reader | File:line | Observes a change when the producer emits a stem instead of a rooted path? | +|---|---|---| +| `EfcFormController.IsValidSelection` | `:1155` → `IsSelectableFolder` `:1151` → `EfcSelectionGuard.IsValidCreationSelection` | **Yes — this is the corrective effect.** `IsValidCreationSelection` returns `false` for any `IsFullOutlookPath` value (`EfcSelectionGuard.cs:76`). A rooted selection currently makes the New-Folder gesture report "Please select a valid folder" (`EfcFormController.cs:819`) even for an in-archive folder. A stem passes. | +| `EfcFormController.ActionOkAsync` | `:743` → `EfcSelectionGuard.IsValidFilingSelection` `:748` | **Yes — this is the corrective effect.** `IsValidFilingSelection` also rejects rooted values (`EfcSelectionGuard.cs:50`). Today, selecting a breadcrumb row whose target is rooted-under-root reaches OK and is refused with "Please select a valid folder." (`:751`). A stem passes the guard and files. | +| `EfcFormController.ButtonCreateClickAsync` | `:505` (guard), `:513`, `:528` | Yes — same guard, plus the value is passed to `FolderHelper.CreateFolderAsync(SelectedFolder, ArchiveRootPath, ...)` (`:527-532`), which concatenates beneath the archive root. A stem is the correct input there; a rooted value was never valid. | +| `EfcFormController.CreateFolderAsync` | `:817` (guard), `:823`, `:835` | Yes — same as above, via `FolderHelper.CreateFolder` (`:834-838`). | +| `EfcFormController.ActionOkAsync` → `_homeController.OpenOlFolderAsync(SelectedFolder)` | `:763` → `EfcHomeController.cs:427-430` → `EfcDataModel.OpenOlFolderAsync` `:299-316` | Yes. `DestinationOlStem = folderpath` verbatim at `EfcDataModel.cs:308`, then `EmailFilerConfig.ResolvePaths()` (`EmailFiler.cs`) enforces `RequireArchiveRelativeStem`. A rooted value would throw there; a stem does not. | +| `_homeController.OpenFsFolderAsync(SelectedFolder)` | `:513`, `:823` → `EfcHomeController.cs:432-435` → `EfcDataModel.OpenFsFolderAsync` `:318-334` | Yes, same shape (`DestinationOlStem` at `:326`). | +| `EfcHomeController.ExecuteMovesCoreAsync` | `EfcHomeController.ExecuteMoves.cs:69` | Yes. The value flows to M3 → M1 → `DestinationOlStem` (`EfcDataModel.cs:287`) → `ResolvePaths` → `RequireArchiveRelativeStem`. This is the #614 D1 leak the fix closes. | +| `EfcHomeController.HandleMoveResult` failure text | `EfcHomeController.ExecuteMoves.cs:134` | Cosmetic only — the message text `"Cannot move to folderpath {selectedFolder}"` would name the stem rather than the rooted path. Note this message **does** embed the value; a stem is strictly less identifying than a rooted store path, so this is a small improvement, not a regression. | +| `EfcHomeController.QuickFileMetrics_WRITE` | `EfcHomeController.Metrics.cs:56` | Yes, cosmetically: the metrics CSV column would carry the stem. Per prior research there are **zero in-repo readers** of that CSV, so no code observes it. | +| `EfcDataModel.MoveToFolderAsync` trash-sentinel test | `EfcDataModel.cs:272` | **No.** The trash pseudo-row is not `IsFullOutlookPath`, so the fix leaves `"Trash to Delete"` byte-identical. | + +**Consumers that do NOT observe any change:** +- Every consumer reached when `_boundRoot` is empty (`BindRowsAsync(rows, scores, ct)`, the 3-argument public overload at `BreadcrumbBridgeRouter.cs:75-82`). The internal 4-argument overload (`:92`) is called from exactly one production site, `EfcFormController.cs:987`. +- P3/P4, the #499 clear-on-rebind pair — they write `null` and read for null-ness only. +- `EfcItemController.SelectedFolder` (`EfcItemController.cs:589-593`) — reads `_itemViewer.GetSelectedFolder()`, a different source entirely; it does not touch `BreadcrumbBridgeRouter`. +- The whole Family-B breadcrumb surface (§5) — it has no `SelectedFolderPath` member. + +**Net assessment:** every behavioral consumer of a stem-valued `SelectedFolderPath` either improves (a previously-refused in-archive selection now files) or is unchanged. No consumer requires a rooted value. The one direction that could regress is the archive-root-exact case becoming a non-selection: today it produces a rooted value that `IsValidFilingSelection` rejects with a dialog; after the fix it produces no selection at all and the prior selection survives. Both outcomes refuse to file the archive root; the fix's outcome is quieter and matches `SelectHierarchyPath`. + +--- + +## 8. The test that pins the current behavior (EXHAUSTIVE, two independent searches) + +### The pinning test + +- File: `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` +- Test method: `Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch`, declared at `:118-119` +- Fixture: `archiveRoot = @"\Archive"` (`:123`), `fullTarget = @"\aRcHiVe\Clients\North"` (`:124`), bound through the internal 4-argument overload (`:146-154`), then `rowSelected` on `row-0` (`:155-158`) +- Assertions: + +```csharp +161 provider.Verify( +162 p => p.ResolveLeafKeyAsync(fullTarget, It.IsAny()), +163 Times.Once +164 ); +165 router.SelectedFolderPath.Should().Be(fullTarget); +``` + +Line **165** is the assertion that pins the defect. Line 161-164 pins a *different and still-correct* property — that the provider lookup uses the original rooted path — and must be preserved: `ToHierarchyPath` (`BreadcrumbBridgeRouter.cs:152-167`) is not changed by this fix. + +**Corrected assertion.** With `archiveRoot = @"\Archive"` and `fullTarget = @"\aRcHiVe\Clients\North"`, `TryMakeArchiveRelative` takes the `StartsWith(OrdinalIgnoreCase)` path (`ArchiveStemContract.cs:131`), the boundary character at index 8 is `\` (`:137-141`), and `stem = fullTarget.Substring(8).TrimStart('\\','/')` = `Clients\North`. The corrected assertion is therefore: + +```csharp +router.SelectedFolderPath.Should().Be(@"Clients\North"); +``` + +The test **name** also asserts the old spec ("RemainsUnchanged") and should be renamed; the XML/inline comment at `:120-122` ("so the provider must receive the original full path unchanged") remains accurate for the `provider.Verify` half and should be kept, narrowed to the provider claim. This is a **deliberate spec correction**: the #439 acceptance criterion that a rooted target survives selection is superseded by #614's archive-relative-stem invariant, which #614 already enforced on the `SelectHierarchyPath` half and at the filing boundary but not on the `SelectRow` half. + +### Exhaustive enumeration of every OTHER test whose assertions depend on `SelectRow` emitting a rooted value, or on the archive-root-exact case being a selection + +#### Search 1 — assertion-side + +Tool: `Grep`, pattern `SelectedFolderPath`, glob `*.cs`, repository root, `output_mode: content`. +Result: 74 lines across 9 files; **7 test files**: `BreadcrumbBridgeRouterTests.Selection.cs` (12), `BreadcrumbBridgeRouterTests.cs` (2), `BreadcrumbBridgeRouterQueueTests.Part2.cs` (24), `BreadcrumbBridgeRouterQueueTests.cs` (4), `BreadcrumbBridgeRouterIssue614Tests.cs` (11), `BreadcrumbBridgeRouterIssue439Tests.cs` (12), and the production files. All 7 test files are in `QuickFiler.Test`; **no test in `UtilitiesCS.Test` or any other test project references the property.** + +#### Search 2 — trigger-side, independently constructed + +Rather than looking at assertions, this search enumerates the **complete set of test-visible entry points that reach `SelectRow`** (per §5 those are exactly: the `rowSelected` inbound message, the public `SelectFirstRow`, and the `Up`/`Down` arrow keys). + +Tool: `Grep`, pattern `rowSelected|SelectFirstRow|\\"key\\":\\"(Up|Down)\\"`, glob `*.cs`, repository root. +Result: **32 matching lines across 10 files.** Removing 4 production lines (`EfcFormController.cs:438`, `BreadcrumbBridgeRouter.cs:196`, `BreadcrumbMessages.cs:28`, `BreadcrumbDocumentAssets.cs:98`) and 4 `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecTests.cs` lines (`:57, :72, :205, :215` — codec parse tests that construct no router) leaves **24 test lines in 6 files**, all in `QuickFiler.Test`. + +#### Agreement + +Both searches converge on the same 6 `QuickFiler.Test` files. Search 2 additionally proves that no test outside `QuickFiler.Test` can reach `SelectRow` at all. + +#### Classification of all 24 SelectRow-reaching test invocations + +The fix changes behavior only when **`_boundRoot` is non-empty AND the selected row's `FilingTarget` is `IsFullOutlookPath` AND that target is at or under the bound root**. Classifying every invocation against those three conditions: + +| Test file:line | Bound root | Selected row's `FilingTarget` | Affected? | +|---|---|---|---| +| `EfcHomeControllerExecuteMovesTests.cs:274` (`SelectFirstRow`) | **empty** (3-arg bind at `:267-271`) | caller-supplied | No — no bound root | +| `BreadcrumbBridgeRouterTests.Selection.cs:32, 48, 64, 78, 137, 150, 255` | **empty** (`Bind()` at `BreadcrumbBridgeRouterTests.cs:113-118`; `BindThreeRows()` at `.Selection.cs:120-127`; local bind at `:245-252`) | `Inbox\Projects\Alpha`, `Inbox\Beta`, banner, `Trash to Delete` | No — no bound root, and none is rooted | +| `BreadcrumbBridgeRouterQueueTests.cs:111, 112, 189, 317, 448` | **empty** (`Bind()` at `:86-96`) | `LeafPath` (relative) | No | +| `BreadcrumbBridgeRouterQueueTests.Part2.cs:208, 233, 260` | **empty** (`Bind()`) | `LeafPath` (relative) | No | +| `BreadcrumbBridgeRouterIssue614Tests.cs:65, 89, 105, 157, 180` (via the `RowSelected()` helper at `:288-291`) | `\\mailbox@example.com\Archive` | `Clients\North` (relative) at `:65,:89,:105,:157`; `\\other@example.org\Archive\Clients` (rooted, **out of root**) at `:180` | No — relative values are untouched; the out-of-root value is still rejected, so `RowSelected_OutOfRootFilingTarget_DoesNotStoreAFullOutlookPath` (`:169`) keeps passing unchanged | +| `BreadcrumbBridgeRouterIssue439Tests.cs:86` | `\Archive` | `Clients\North` (relative) | No | +| `BreadcrumbBridgeRouterIssue439Tests.cs:156` | `\Archive` | `\aRcHiVe\Clients\North` (**rooted, under root**) | **YES** — the pinning test | +| `BreadcrumbBridgeRouterIssue439Tests.cs:233` | `\Archive` | `Clients\Canceled` (relative) | No | +| `BreadcrumbBridgeRouterIssue439Tests.cs:295` | `\\mailbox@example.com\Archive` | `Clients\North` (relative) | No | + +#### Result — the countable population + +- **Tests whose assertions must change: exactly 1** — `BreadcrumbBridgeRouterIssue439Tests.Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch`, one assertion line (`:165`), plus a name/comment correction. +- **Tests that depend on the archive-root-exact case being a `SelectRow` selection: exactly 0.** No test binds a presented row whose `FilingTarget` equals the bound archive root. The two tests that assert a root-valued `SelectedFolderPath` — `Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection` (`:619`, assertion `:665` `Be(@"\Archive")`) and `Issue614Tests.SegmentActivate_WithNoBoundArchiveRoot_PreservesThePassThroughMode` (`:188`, assertion `:221` `Be(@"\Archive")`) — both go through **`SelectHierarchyPath` under an empty `_boundRoot`**, not through `SelectRow`. `Issue439SlashOnlyArchiveRoot...` binds with `@"\"` (`:645`), which `BindRowsAsync` trims to the empty string at `BreadcrumbBridgeRouter.cs:107-109`, so `_boundRoot.Length == 0`. Neither is affected. +- **Tests asserting no-bound-root pass-through: 2** (the two named immediately above). Both must be preserved unchanged, since the fix does not touch the `_boundRoot.Length == 0` short-circuit. + +--- + +## 9. The composition test from the #614 remediation + +- File: `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` +- Test method: `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary`, declared at `:167-168` +- Shape: a 10-element candidate matrix (`:172-184`) is filtered through `EfcSelectionGuard.IsValidFilingSelection` (`:189`); every value that passes is placed into an `EmailFilerConfig` with `OlAncestor = @"\\mailbox@example.com\Archive"` (`:197`) and `DestinationOlStem = candidate` (`:198`), and `config.ResolvePaths()` is asserted not to throw (`:204-208`). A final assertion (`:211-213`) requires at least one candidate to have been evaluated. + +**Confirmation the proposed fix keeps it passing.** The test exercises no router at all — it composes `EfcSelectionGuard` directly against `EmailFilerConfig.ResolvePaths`. The fix modifies neither `EfcSelectionGuard` nor `EmailFilerConfig`, so the test is structurally untouched. Semantically it also stays green and becomes *more* meaningful: the fix guarantees the producer now emits only values in the accepted class (relative stems, `Trash to Delete`) or nothing at all, which is precisely the precondition this composition test asserts about the guard's accepted set. + +Two adjacent tests in the same file explicitly record the deferral this issue closes and will need their rationale comments revisited (they remain factually correct as *guard-surface* claims, since the guard still rejects rooted values — the producer simply stops producing them): + +- `IsValidFilingSelection_RootedTargetUnderArchiveRoot_IsRejected` (`:143-154`), whose comment at `:146` reads "normalization is deferred to issue #637" and whose `because` string at `:152` repeats it. +- The same deferral is recorded in production at `QuickFiler/Controllers/EfcSelectionGuard.cs:30`: `"Producer-side normalization in BreadcrumbBridgeRouter.SelectRow is deferred to issue #637."` This comment becomes stale on merge and should be updated. + +--- + +## 10. The OK chain and the `ArchiveRootPath` throw + +### The hops, with file and line + +| Hop | File:line | Code | +|---|---|---| +| H1 | `QuickFiler/Controllers/EfcFormController.cs:418` | `_formViewer.Ok.Click += ButtonOK_Click;` | +| H2 | `QuickFiler/Controllers/EfcFormController.cs:460` | `public async void ButtonOK_Click(object sender, EventArgs e) => await ButtonOkClickAsync();` | +| H3 | `QuickFiler/Controllers/EfcFormController.cs:462-475` | `ButtonOkClickAsync` — `try { ... await ActionOkAsync(); } catch (System.Exception ex) { BoundaryErrorSink(ex.Message, ex); }` | +| H4 | `QuickFiler/Controllers/EfcFormController.cs:738-772` | `ActionOkAsync` — guard at `:745-753`, `_formViewer.Hide()` at `:756`, `await _homeController.ExecuteMovesAsync()` at `:759`, `_formViewer.Dispose(); Cleanup();` at `:769-770` | +| H5 | `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:32-47` | `ExecuteMovesAsync` | +| H6 | `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:67-87` | `ExecuteMovesCoreAsync` → `MoveToFolderAsync` at `:78` | +| H7 | `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:89-112` | M3 forwarder → `_dataModel.MoveToFolderAsync(...)` at `:98` | +| H8 | `QuickFiler/Controllers/EfcDataModel.cs:259-297` | M1; `OlAncestor = Globals.Ol.ArchiveRootPath` at `:289` | +| H9 | `TaskMaster/AppGlobals/AppOlObjects.cs:253-267` | `ArchiveRootPath` getter | +| H10 | `TaskMaster/AppGlobals/ArchiveRootPathGuard.cs:32-60` | `RequireResolvedArchiveRoot` | + +### (a) Is `ButtonOK_Click` `async void`, and does it rethrow? + +**`async void`: confirmed. Rethrows: NOT confirmed — the opposite is true on the current tree.** + +```csharp +460 public async void ButtonOK_Click(object sender, EventArgs e) => await ButtonOkClickAsync(); +461 +462 internal async Task ButtonOkClickAsync() +463 { +464 try +465 { +466 if (SynchronizationContext.Current is null) +467 SynchronizationContext.SetSynchronizationContext(_formViewer.UiSyncContext); +468 +469 await ActionOkAsync(); +470 } +471 catch (System.Exception ex) +472 { +473 BoundaryErrorSink(ex.Message, ex); +474 } +475 } +``` + +`BoundaryErrorSink` is an injectable seam defaulting to the log4net logger (`EfcFormController.cs:127-129`). It swallows the exception. Therefore an `InvalidOperationException` from `ArchiveRootPath` reached through the **OK button** is logged and does not crash the message pump. The issue's claim that it "becomes an unhandled UI-thread exception" is incorrect for that entry point. + +**The real OK-button defect is a silent half-completed teardown.** `ActionOkAsync` calls `_formViewer.Hide()` at `:756` *before* `await _homeController.ExecuteMovesAsync()` at `:759`, and `_formViewer.Dispose(); Cleanup();` at `:769-770` *after*. When the await throws, lines 769-770 never run: the EFC form is hidden, undisposed, and uncleaned, and the user sees no message — only a log entry. The item is not filed and nothing says so. + +**Two entry points do lack the catch and remain genuinely unhandled:** +- `EfcFormController.cs:392` — `new KaKeyAsync("Collection", Keys.Return, (k) => ActionOkAsync())` in `RegisterAlwaysOnAsyncKeyActions` (`:383-395`). This registers `ActionOkAsync` directly, bypassing `ButtonOkClickAsync`. +- `EfcFormController.cs:623` and `:683` — `KbdExecuteAsync(ActionOkAsync)` for the `'K'` character action. `KbdExecuteAsync` is declared at `:894-898` and `:900-904` and contains **no** try/catch. + +### (b) Does `ExecuteMovesAsync` use try/finally with no catch? + +**Confirmed.** `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:32-47`: + +```csharp + 32 public async Task ExecuteMovesAsync() + 33 { + 34 if (!TryBeginExecuteMoves()) + 35 { + 36 return; + 37 } + 38 + 39 try + 40 { + 41 await ExecuteMovesCoreAsync(); + 42 } + 43 finally + 44 { + 45 ResetExecuteMovesState(); + 46 } + 47 } +``` + +The `finally` releases the `Interlocked` re-entrancy guard (`:54-65`), so a throw does **not** wedge the guard. It does not observe or translate the exception. + +### (c) Does `ArchiveRootPath` throw `InvalidOperationException` on unresolvable / cross-store? + +**Confirmed.** `TaskMaster/AppGlobals/AppOlObjects.cs:253-267`: + +```csharp +253 public string ArchiveRootPath +254 { +255 get +256 { +257 if (_archiveRootPath is null) +258 { +259 _archiveRootPath = ArchiveRootPathGuard.RequireResolvedArchiveRoot( +260 Path.Combine(Root.FolderPath, "Archive"), +261 ArchiveRoot?.FolderPath, +262 message => logger.Error(message) +263 ); +264 } +265 return _archiveRootPath; +266 } +267 } +``` + +`TaskMaster/AppGlobals/ArchiveRootPathGuard.cs`: + +```csharp + 38 if ( + 39 string.IsNullOrWhiteSpace(composedArchiveRootPath) + 40 || string.IsNullOrWhiteSpace(resolvedArchiveFolderPath) + 41 ) + 42 { + 43 logDiagnostic?.Invoke(UnresolvableRule); + 44 throw new InvalidOperationException(UnresolvableRule); + 45 } + 46 + 47 if ( + 48 !string.Equals( + 49 composedArchiveRootPath, + 50 resolvedArchiveFolderPath, + 51 StringComparison.OrdinalIgnoreCase + 52 ) + 53 ) + 54 { + 55 logDiagnostic?.Invoke(CrossStoreRule); + 56 throw new InvalidOperationException(CrossStoreRule); + 57 } +``` + +Message constants at `:13-17`. The value is cached only on success (`AppOlObjects.cs:259`, assignment happens after the call returns), so the throw **recurs on every read** rather than being cached. + +Note the throw is not confined to `MoveToFolderAsync`: within the EFC chain, `EfcFormController.cs:987` reads `ArchiveRootPath` for `BindRowsAsync` (inside a `try/catch` at `:989-995`), and `:529`, `:539`, `:836`, `:846` read it inside the create paths. + +### The narrowest seam for a benign degrade + +The narrowest production seam is **`EfcHomeController.ExecuteMovesAsync` (`ExecuteMoves.cs:32-47`)**, because: +- It is the single funnel for the OK-filing path, reached from all three OK entry points (button, `Keys.Return`, `'K'`). +- It already owns a `try` block, so adding a `catch (InvalidOperationException)` is a one-clause change with no restructuring and no new file. +- It sits *below* `ActionOkAsync`'s `Hide`/`Dispose`/`Cleanup` sequence, so catching there lets `ActionOkAsync` continue to line 769-770 and complete the teardown deterministically instead of leaving the form hidden-and-undisposed. +- It is already unit-testable headlessly: `QuickFiler.Test/Controllers/EfcHomeControllerExecuteMovesTests.cs` constructs `EfcHomeController` and drives `ExecuteMovesAsync`/`MoveToFolderAsync` through the injected `MoveToFolderAsyncAction` seam without Outlook. + +A narrower alternative — catching inside `EfcDataModel.MoveToFolderAsync` (M1) at the `ArchiveRootPath` read — is worse: `EfcDataModel.cs` carries no `#nullable enable`, has no injectable notification seam, and M1's `return false` contract would silently route to the "Cannot move to folderpath X" message, conflating an archive-root configuration failure with an ordinary move failure. + +### Existing UI-notification pattern for aborting a filing operation + +The repository already has a first-class, injectable pattern for exactly this. Cited example, `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs`: + +```csharp + 23 internal Action MoveFailureMessageAction { get; set; } = + 24 text => MessageBox.Show(text); +``` + +used at `:132-136`: + +```csharp +132 if (!result) +133 { +134 MoveFailureMessageAction($"Cannot move to folderpath {selectedFolder}"); +135 return; +136 } +``` + +This is the pattern to reuse: an `internal Action<...>` property on the controller with a `MessageBox.Show` default, so production shows a dialog and tests assert on a captured string without a UI. A second instance of the same pattern with a richer signature exists on the same type at `QuickFiler/Controllers/EfcHomeController.cs:299-305`: + +```csharp +299 internal Action< +300 string, +301 string, +302 MessageBoxButtons, +303 MessageBoxIcon +304 > MessageBoxShowAction { get; set; } = +305 (text, caption, buttons, icon) => MessageBox.Show(text, caption, buttons, icon); +``` + +A third instance of the same idiom exists in `QuickFiler/Controllers/QfcItemController.MailActions.cs:31`. The planner should not invent a new notification shape; one of these two `EfcHomeController` seams is the right vehicle. + +**Redaction constraint:** any new user-facing message must not embed the archive root path. `ArchiveRootPathGuard.UnresolvableRule` and `CrossStoreRule` (`ArchiveRootPathGuard.cs:13-17`) are already redacted, value-free strings and are the appropriate text to surface. + +--- + +## 11. Test project and framework facts + +### Ownership + +| Affected production file | Owning test project | Test files | +|---|---|---| +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | **`QuickFiler.Test`** | `Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, `Controllers/BreadcrumbBridgeRouterIssue614Tests.cs`, `Controllers/BreadcrumbBridgeRouterTests.cs` + `.Selection.cs`, `Controllers/BreadcrumbBridgeRouterQueueTests.cs` + `.Part2.cs` | +| `QuickFiler/Controllers/EfcSelectionGuard.cs` (comment only) | `QuickFiler.Test` | `Controllers/EfcSelectionGuardTests.cs` | +| `QuickFiler/Controllers/EfcDataModel.cs` (if §6 convergence is in scope) | `QuickFiler.Test` | no dedicated `EfcDataModelTests.cs` exists — this is a coverage gap, not an existing suite | +| `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs` (if §10 degrade is in scope) | `QuickFiler.Test` | `Controllers/EfcHomeControllerExecuteMovesTests.cs`, `Controllers/EfcHomeControllerTests.cs` | +| `UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs` (no change expected) | `UtilitiesCS.Test` | `OutlookObjects/Folder/ArchiveStemContractTests.cs` | + +`UtilitiesCS.Test` owns **no** test that reaches `BreadcrumbBridgeRouter` (§8, Search 2). No other test project is involved. + +### Framework confirmation for the specific files that will change + +`QuickFiler.Test/packages.config`: `FluentAssertions` 8.10.0 (`:8`), `Moq` 4.20.72 (`:112`), `MSTest.Analyzers` (`:114`), `MSTest.TestAdapter` 4.3.3 (`:119`), `MSTest.TestFramework` 4.3.3 (`:120`), all `targetFramework="net481"`. +`UtilitiesCS.Test/packages.config`: identical versions at `:9`, `:139`, `:141`, `:146`, `:147`. + +In `BreadcrumbBridgeRouterIssue439Tests.cs` specifically (the file that changes), all three are in use at `:4` (`using FluentAssertions;`), `:5` (`using Microsoft.VisualStudio.TestTools.UnitTesting;`), `:6` (`using Moq;`), with `[TestClass]` at `:17` and `[TestMethod]` at `:20, :118, :168, :257, :306, :353, :420, :496, :541, :618`. + +### The existing archive-root binding pattern in the router tests — do not invent a new fixture + +There are **two** established shapes; both are legitimate and neither requires any new infrastructure. + +**Shape 1 — per-test local construction (used by `BreadcrumbBridgeRouterIssue439Tests.cs`, the file that changes).** Strict mocks, constructed inline, bound through the **internal 4-argument** `BindRowsAsync` overload whose fourth positional argument is the archive root. Concrete cited example, `BreadcrumbBridgeRouterIssue439Tests.cs:123-158`: + +```csharp +123 const string archiveRoot = @"\Archive"; +124 const string fullTarget = @"\aRcHiVe\Clients\North"; +125 var provider = new Mock(MockBehavior.Strict); +126 var host = new Mock(MockBehavior.Strict); +127 host.SetupGet(h => h.IsCoreInitialized).Returns(true); +128 host.Setup(h => h.NavigateToString(It.IsAny())); +129 host.Setup(h => h.PostMessageJson(It.IsAny())); +130 FolderTreeNodeKey key = Key(fullTarget); +131 provider +132 .Setup(p => p.ResolveLeafKeyAsync(fullTarget, It.IsAny())) +133 .ReturnsAsync(key); +134 provider +135 .Setup(p => p.GetAncestorChainAsync(key, It.IsAny())) +136 .ReturnsAsync(Chain(fullTarget, "Clients", "North")); +137 var router = new BreadcrumbBridgeRouter( +138 provider.Object, +139 host.Object, +140 new BreadcrumbMessageCodec(), +141 new BreadcrumbHtmlRenderer(), +142 new BreadcrumbOutboundQueue(host.Object) +143 ); +144 +145 // Act +146 router +147 .BindRowsAsync( +148 new[] { fullTarget }, +149 new[] { new FolderScore(fullTarget, 730, 0.73) }, +150 archiveRoot, +151 CancellationToken.None +152 ) +153 .GetAwaiter() +154 .GetResult(); +155 router +156 .ProcessInboundAsync("{\"type\":\"rowSelected\",\"rowId\":\"row-0\"}") +157 .GetAwaiter() +158 .GetResult(); +``` + +Local helpers in that file: `Key(string)` at `:668-671`, `Chain(string, string, string)` at `:673-687`, `Segment(string, string, bool)` at `:689-692`. + +**Shape 2 — `[TestInitialize]` fixture with a `BindChain` helper and a log4net `MemoryAppender` (used by `BreadcrumbBridgeRouterIssue614Tests.cs`).** Preferable when the test must assert the *rejection diagnostic* rather than only the selection value. Cited: `Setup()` at `:38-52`, `Cleanup()` at `:54-58`, `BindChain(...)` at `:236-262` (which passes `ArchiveRoot` as the fourth `BindRowsAsync` argument at `:257`), `BindStandardChain()` at `:224-234`, `Inbound(json)` at `:264-267`, JSON builders at `:269-291`, `AssertRejectionDiagnosticWithoutIdentifiers` at `:310-326`, and appender attach/detach at `:338-356`. + +The `internal` 4-argument overload is visible to `QuickFiler.Test` (it is used from 14 sites there), so no `InternalsVisibleTo` change is required. + +**Empty-root note the planner must respect:** to produce `_boundRoot.Length == 0` in a test, pass `string.Empty`, `null`, whitespace, **or a separator-only value** such as `@"\"` — `BindRowsAsync` at `BreadcrumbBridgeRouter.cs:107-109` applies `TrimEnd('\\','/')`, so `@"\"` becomes empty. `BreadcrumbBridgeRouterIssue439Tests.cs:645` relies on exactly this. + +--- + +## 12. Nullable posture of each production file that may change + +`/p:TreatWarningsAsErrors=true` promotes `CS86xx` only in files carrying a `#nullable enable` directive (this repository has no `Directory.Build.props` and no `` element in any project). + +| Production file | `#nullable enable`? | Consequence for edited lines | +|---|---|---| +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | **YES** — line 1 | `CS86xx` become build errors. The new `out string stem` local is non-nullable and definitely assigned by `TryMakeArchiveRelative` on both paths (`ArchiveStemContract.cs:112`), so no warning is expected; but `SelectedFolderPath` is `string?` (`BreadcrumbBridgeRouter.cs:59`) and `CommitSelection` takes a non-nullable `string` (`:131`) — do not introduce a nullable temporary. | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | **YES** — line 1 | same | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | **YES** — line 1 | same | +| `QuickFiler/Controllers/EfcSelectionGuard.cs` | **YES** — line 1 | comment-only change expected; no diagnostic risk | +| `UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs` | **YES** — line 1 | no change expected | +| `UtilitiesCS/EmailIntelligence/EmailParsingSorting/EmailFilerConfig.cs` | **YES** — line 1 | no change expected | +| `QuickFiler/Controllers/EfcDataModel.cs` | **NO** | file does not participate in nullable analysis; `CS86xx` will not be promoted on edited lines | +| `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs` | **NO** | as above | +| `QuickFiler/Controllers/EfcHomeController.cs` | **NO** | as above | +| `QuickFiler/Controllers/EfcFormController.cs` | **NO** | as above | +| `TaskMaster/AppGlobals/AppOlObjects.cs` | **NO** | as above | +| `TaskMaster/AppGlobals/ArchiveRootPathGuard.cs` | **NO** | as above | + +Derivation: `Grep` for `^#nullable` across `QuickFiler/` returns 26 files, all of which are breadcrumb-related plus `EfcSelectionGuard.cs`; none of `EfcDataModel.cs`, `EfcFormController.cs`, `EfcHomeController*.cs` appears. Separate targeted greps confirmed `ArchiveStemContract.cs:1` and `EmailFilerConfig.cs:1`. + +**File-size headroom (500-line limit).** `BreadcrumbBridgeRouter.Selection.cs` is 209 lines; `BreadcrumbBridgeRouter.cs` is 304; `BreadcrumbBridgeRouter.Arrows.cs` is 211; `EfcHomeController.ExecuteMoves.cs` is 147; `BreadcrumbBridgeRouterIssue439Tests.cs` is 694 lines — **already over the 500-line limit**, so no material additions should be made to that test file; a corrected assertion is a substitution and does not worsen it, but any *new* regression tests belong in a new file or in `BreadcrumbBridgeRouterIssue614Tests.cs` (358 lines). + +--- + +## 13. Open questions the planner must decide (report-only; no plan proposed here) + +1. **Scope of the third finding.** §10 shows the issue's premise (unhandled UI-thread exception via `ButtonOK_Click`) is inaccurate for the button path but accurate for the two keyboard paths, and that the real button-path defect is a half-completed teardown. Whether to address that in this issue or promote it separately is a scoping decision. +2. **Whether the `string`/`MAPIFolder` convergence in §6 is in scope.** It is a genuinely separate change to a non-nullable file with no existing test class, and it is not required to close D1. +3. **Stale comment `EfcSelectionGuard.cs:30`** and the two `EfcSelectionGuardTests.cs` rationale strings (`:146`, `:152`) reference "#637 deferred" and become inaccurate on merge. +4. **`OpenOlFolderAsync` / `OpenFsFolderAsync`** (`EfcDataModel.cs:299-334`) assign `DestinationOlStem` verbatim exactly as M1 does and read `ArchiveRootPath` in the same way. They are reached from `ActionOkAsync:763` and `ButtonCreateClickAsync:513` / `CreateFolderAsync:823`. They are outside the literal wording of #637 but share its defect class. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md new file mode 100644 index 000000000..82a3685b1 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md @@ -0,0 +1,1054 @@ +# 2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed (Spec) + +- **Issue:** #637 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-08-29T12-20 +- **Status:** Draft +- **Version:** 0.1 + +## Context +Issue #614 established the invariant that `SelectedFolderPath` is an archive-relative stem, and +created `ArchiveStemContract` to express it. The invariant is enforced at the filing boundary, where +`EmailFilerConfig.ResolvePaths` calls `RequireArchiveRelativeStem`, but it is not enforced at the +producer. `BreadcrumbBridgeRouter.SelectRow` still commits a rooted filing target verbatim when that +target is at or under the bound archive root, so a rooted value can still become +`SelectedFolderPath`. That is defect D1 half-closed: the store-root and cross-store leaks are +stopped, but rootedness as such still escapes the producer. + +Two things make this worth fixing rather than tolerating. + +First, it left a live trap that has already fired once. During #614 remediation cycle 1 the OK-path +guard was widened to accept rooted under-root values so that it would agree with `SelectRow`. Because +nothing between the guard and the filing boundary normalizes the value, the accepted value reached +`RequireArchiveRelativeStem` and threw. `ButtonOK_Click` is `async void` and rethrows, and +`ExecuteMovesAsync` wraps its core in try/finally with no catch, so the `ArgumentException` became an +unhandled UI-thread exception after the form had already been hidden. The re-audit caught it and the +change was reverted. The underlying asymmetry that made the widening look reasonable is still +present. + +Second, the D8 normalizer is only half-wired. `EfcDataModel.ToArchiveRelativeStem` exists and is +correct, but it is called only from the `MAPIFolder` overload of `MoveToFolderAsync`. The `string` +overload assigns `DestinationOlStem = folderpath` verbatim, so it performs no normalization at all. +Any rooted value arriving through that overload depends entirely on the boundary throw. + +The fix is to normalize at the producer: in `SelectRow`, when `TryMakeArchiveRelative` succeeds with +a non-empty stem, commit the stem rather than the rooted input; when it succeeds with an empty stem +the value is the archive root itself, which `SelectHierarchyPath` already treats as a deterministic +non-selection and `SelectRow` should too. Once the producer cannot emit a rooted value, the OK guard +and the filing boundary agree by construction rather than by coincidence, and the composition test +added during remediation keeps them agreeing. + +This also requires updating the existing test that asserts a rooted input survives selection, so that +it asserts the stem instead. That is a deliberate spec correction of the same kind #614 already +applied twice, and should be recorded as such rather than treated as a weakened test. + +Environment: +- OS/version: Windows 11 Pro 10.0.26200; .NET Framework 4.8.1 VSTO add-in. +- Python version: Not applicable; this is C#. +- Command/flags used: Static tracing during the issue #614 remediation re-audit, plus the failing + path reproduced by remediation cycle 1. +- Data source or fixture: Repository source on the issue #614 branch. + +Impact / Severity: +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +No user-visible defect on the shipped code: a rooted selection is rejected at the OK guard with a +clear dialog. Severity is Medium because the asymmetry is a live trap for future changes, as +demonstrated by remediation cycle 1, and because the half-wired D8 normalizer means one filing +overload relies entirely on a throw. + + +## Repro & Evidence +Steps to Reproduce: +1. Bind breadcrumb rows with an archive root, and present a suggestion row whose filing target is a + rooted path at or under that root. `FolderPredictor.ProjectSuggestionPath` strips the archive + prefix only when the suggestion is strictly under it, so a suggestion whose folder is the archive + root is returned rooted and verbatim. +2. Select that row. `BreadcrumbBridgeRouter.SelectRow` commits the rooted value to + `SelectedFolderPath`. +3. Observe that the value reaching the filing boundary is rooted, and is rejected there by + `RequireArchiveRelativeStem` rather than having been normalized at the producer. + +Expected: +`SelectedFolderPath` is always an archive-relative stem. The producer normalizes; the boundary guard +is a backstop that never fires in normal operation. A row whose filing target is the archive root +itself is a non-selection, consistently with `SelectHierarchyPath`. + +Actual: +`SelectRow` commits a rooted value verbatim. The invariant is enforced only at the boundary, where +violating it is an exception rather than a corrected value. + +Logs / Screenshots: +- [ ] Attached minimal logs or screenshot +- Snippet: not applicable; established by source tracing. See `BreadcrumbBridgeRouter.SelectRow`, the + `string` overload of `EfcDataModel.MoveToFolderAsync`, and + `ArchiveStemContract.RequireArchiveRelativeStem`. + + +## Scope & Non-Goals + +Evidence base: `research/research.2026-08-29T12-30.md`, produced against this branch at +`ecdb1c84ba8541ab67042985919cfed4df768c01`. Every count and line citation reused below was +independently re-verified against the working tree while authoring this spec; the two corrections +found during that re-verification are recorded in "Corrections to the research file" under Root +Cause Analysis. + +### In scope — four changes (A-D) + +**A. Producer normalization in `SelectRow`.** +In `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`, bind the currently discarded +`out _` of `ArchiveStemContract.TryMakeArchiveRelative` (line 99), commit the stem when it is +non-empty, and treat an empty stem — the archive-root-exact case — as a deterministic +non-selection: an early `return` that leaves the prior selection untouched, raises no +`SelectedFolderPathChanged` event, and emits a value-free diagnostic, exactly as +`SelectHierarchyPath` already does at lines 119-126. + +This change must remain **nested inside the existing `ArchiveStemContract.IsFullOutlookPath(selection)` +arm** of the guard at lines 96-100. A method-wide rewrite of the form "commit only when +`TryMakeArchiveRelative` succeeds" would reject every ordinary relative suggestion target and the +`Trash to Delete` pseudo-row, because `TryMakeArchiveRelative` returns `false` for both (verified: +the `StartsWith` test at ArchiveStemContract.cs:129-135 fails for a relative value). The existing +no-bound-root pass-through short-circuit — the first conjunct `_boundRoot.Length != 0` at line 97 — +is preserved unchanged. + +**B. Normalization in the `string` overload of `MoveToFolderAsync`.** +In `QuickFiler/Controllers/EfcDataModel.cs`, the `string` overload (declared at :259-265) assigns +`DestinationOlStem = folderpath` verbatim at :287. The normalization is introduced behind a small, +pure, directly unit-testable `internal static` helper on `EfcDataModel` that takes the candidate +path and the archive ancestor and returns the value to assign; the assignment site at :287 calls +that helper. + +The helper is **gated on `IsFullOutlookPath`**: a value that is not a full Outlook path — which +covers every ordinary relative stem and the `"Trash to Delete"` sentinel the same method branches +on at :272 — is returned verbatim and byte-identical. + +A helper rather than an inline call is required for testability. The enclosing overload constructs a +real `EmailFiler` and awaits `SortAsync` (EfcDataModel.cs:293-294), so it cannot be driven +headlessly; a pure static helper can, through the existing `InternalsVisibleTo("QuickFiler.Test")` +seam that `EfcDataModelIssue614Tests` already uses. + +**C. Test spec correction.** +The single existing assertion that pins the defect — +`QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs:165`, +`router.SelectedFolderPath.Should().Be(fullTarget);` inside +`Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch` (:118-166) — is +corrected to assert the stem `@"Clients\North"`. The test name and the arrange comment at :121-122 +are narrowed so they no longer assert the superseded spec. + +This is recorded as a **deliberate spec correction**, not a weakened test. The issue #439 criterion +that a rooted target survives selection is superseded by issue #614's archive-relative-stem +invariant, which #614 enforced on the `SelectHierarchyPath` half (Selection.cs:119-128) and at the +filing boundary (EmailFilerConfig.ResolvePaths) but not on the `SelectRow` half. The companion +assertion in the same test at :161-164 — `provider.Verify(p => p.ResolveLeafKeyAsync(fullTarget, ...))` +— pins a different and still-correct property, that the provider lookup uses the original rooted +path, and must be preserved. + +**D. Stale-comment cleanup.** +Three locations record that producer-side normalization is deferred to issue #637 and become +inaccurate the moment this merges. Verified locations (re-derived, see the census below): + +| # | Location | Current text | +|---|---|---| +| 1 | `QuickFiler/Controllers/EfcSelectionGuard.cs:30` | `/// normalization in BreadcrumbBridgeRouter.SelectRow is deferred to issue #637.` | +| 2 | `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146` | `// RC-1 inversion: rooted values are never filing stems here; normalization is deferred to issue #637.` | +| 3 | `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:152` | the `because` string `"...and producer-side normalization is deferred to issue #637"` | + +The guard-surface claims those strings make remain factually true — the guard still rejects rooted +values, and `EfcSelectionGuard` behavior is not changed by this work. Only the deferral wording is +stale and must be replaced with a statement that the producer now normalizes. + +### Out of scope / non-goals — promoted to GitHub issue #695 + +1. **The `Globals.Ol.ArchiveRootPath` benign-degrade item** listed as the third checkbox in the + issue's proposed-fix list, together with the unhandled keyboard entry points to `ActionOkAsync` + and the half-completed form teardown on the button path. +2. **The verbatim `DestinationOlStem` assignments in `EfcDataModel.OpenOlFolderAsync` (:308) and + `OpenFsFolderAsync` (:326).** These share the defect class of change B but are reached from + different callers, are not part of the `MoveToFolderAsync` family, and are not required to close + D1. + +**Factual correction that drove exclusion 1.** Issue #637 asserts that an `InvalidOperationException` +from `ArchiveRootPath` "becomes an unhandled UI-thread exception" because `ButtonOK_Click` is +`async void` and rethrows. Research §10 establishes, and this spec's author re-verified at +EfcFormController.cs:460-475, that the premise is inaccurate against the current tree: + +```csharp +460 public async void ButtonOK_Click(object sender, EventArgs e) => await ButtonOkClickAsync(); +462 internal async Task ButtonOkClickAsync() +463 { +464 try { ... await ActionOkAsync(); } +471 catch (System.Exception ex) +473 { BoundaryErrorSink(ex.Message, ex); } +475 } +``` + +`ButtonOK_Click` delegates to `ButtonOkClickAsync`, which catches all exceptions into the injectable +`BoundaryErrorSink` (default at EfcFormController.cs:127-129), so on the button path the exception +is **logged, not unhandled**. The genuine defects on that chain are different in kind — a +half-completed teardown on the button path (`_formViewer.Hide()` at EfcFormController.cs:756 runs +before the throwing await at :759, while `Dispose()`/`Cleanup()` at :769-770 never run), and two +genuinely uncaught keyboard entry points (EfcFormController.cs:392 `KaKeyAsync(... Keys.Return, k => +ActionOkAsync())`, and `KbdExecuteAsync(ActionOkAsync)` at :623 and :683, whose declarations at +:894-904 contain no try/catch). Each needs its own user-experience decision about what aborting a +filing operation should look like, and its own tests. Tracked in issue #695. Evidence: research §10. + +### Explicitly excluded systems, integrations, and datasets + +- `EfcSelectionGuard` predicate logic (`IsValidFilingSelection`, `IsValidCreationSelection`) — + comment-only change; both predicates keep rejecting rooted values. +- `ArchiveStemContract` (UtilitiesCS) — no production change; it already implements the required + semantics. +- `EmailFilerConfig.ResolvePaths` / `RequireArchiveRelativeStem` — the boundary backstop is + unchanged; this work makes it stop firing rather than removing it. +- The `MAPIFolder` overload of `MoveToFolderAsync` (EfcDataModel.cs:336-357) and + `EfcDataModel.ToArchiveRelativeStem` (:372-386) — unchanged. +- `EfcHomeController.ExecuteMovesAsync` / `ExecuteMovesCoreAsync` — no new catch clause; that is + issue #695. +- `TaskMaster/AppGlobals/AppOlObjects.cs` and `ArchiveRootPathGuard.cs` — unchanged. +- The Family-B breadcrumb surface (`SelectRow(int index)` on `BreadcrumbStateModel`, + `BreadcrumbSelectionSession`, `FolderBreadcrumbBridgeRouter`, `BreadcrumbBridgeCoordinator`) — + an unrelated ItemViewer drop-down selector with no `SelectedFolderPath`, no `_boundRoot`, and no + `ArchiveStemContract` reference. Not touched. +- The Outlook object model, Graph, and all filesystem/CSV outputs — no change. + +## Root Cause Analysis + +### The mechanism in `SelectRow` + +`QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs:83-107` (verbatim, re-read against the +tree): + +```csharp + 90 string selection = + 91 row.Kind == BreadcrumbRowKind.TrashPseudoRow + 92 ? BreadcrumbRowBuilder.TrashRowText + 93 : row.FilingTarget; + 94 // #614 D2: reject only an out-of-root FULL Outlook target; a rooted target at or + 95 // under the root passes verbatim (#439) and no bound root leaves the row unguarded. + 96 if ( + 97 _boundRoot.Length != 0 + 98 && ArchiveStemContract.IsFullOutlookPath(selection) + 99 && !ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out _) +100 ) +101 { +102 log.Error("Breadcrumb row rejected: target is outside the archive root."); +103 return; +104 } +105 +106 CommitSelection(row, selection); +``` + +The guard is a **three-term conjunction, and the third term is negated**: + +1. `_boundRoot.Length != 0` — the deliberate #439 pass-through mode when no archive root is bound. +2. `ArchiveStemContract.IsFullOutlookPath(selection)` — restricts the guard to rooted values. +3. `!ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out _)` — **this is the term + that lets a rooted at-or-under-root value through.** `TryMakeArchiveRelative` returns `true` for + any path at or under the root, so its negation is `false` and the conjunction fails: the guard + does not fire and control falls through to `CommitSelection(row, selection)` at line 106, which + commits the **input**, not the stem. The stem is computed and then discarded into `out _`. + +`CommitSelection` (:131-139) is the sole non-clearing write site: it sets `SelectedFolderPath = +selection` (:134) and raises `SelectedFolderPathChanged` (:138). + +### Truth table of `TryMakeArchiveRelative` outcomes that matters for the fix + +Derived from ArchiveStemContract.cs:106-145, re-read against the tree. + +| `selection` shape | term 1 | `IsFullOutlookPath` | `TryMakeArchiveRelative` | `stem` | guard fires? | committed today | required after fix | +|---|---|---|---|---|---|---|---| +| Rooted, strictly under root | true | true | **true** (:143-144) | non-empty | no | rooted value, verbatim | **the stem** | +| Rooted, exactly equal to root | true | true | **true** (:124-127) | `string.Empty` | no | rooted root, verbatim | **deterministic non-selection** | +| Rooted, out of root / cross-store | true | true | false (:131) | `string.Empty` | **yes** | nothing (rejected) | unchanged — still rejected | +| Rooted, separator-boundary near miss (`\Archive2\...`) | true | true | false (:137-141) | `string.Empty` | **yes** | nothing (rejected) | unchanged — still rejected | +| Relative stem (`Clients\North`) | true | **false** | not evaluated (short-circuit) | n/a | no | relative value, verbatim | unchanged — verbatim | +| `Trash to Delete` pseudo-row | true | **false** | not evaluated | n/a | no | `Trash to Delete` | unchanged — byte-identical | +| Any value, no bound root | **false** | not evaluated | not evaluated | n/a | no | verbatim | unchanged — verbatim | + +Two rows of this table are load-bearing in opposite directions. Row 2 returning `true` with an +empty stem is the exact mechanism by which the archive-root-exact value escapes the negated third +conjunct. Rows 5 and 6 returning `false` are the exact reason the fix must stay nested inside the +`IsFullOutlookPath` arm rather than becoming a method-wide rewrite. + +The out-parameter contract that makes the fix safe: `TryMakeArchiveRelative` assigns +`stem = string.Empty` unconditionally on entry (ArchiveStemContract.cs:112), so `stem` is definitely +assigned on every exit path and is never the input value. + +### The mechanism in the `string` overload of `MoveToFolderAsync` + +`QuickFiler/Controllers/EfcDataModel.cs:282-291` builds the `EmailFilerConfig` with +`DestinationOlStem = folderpath` at :287 — verbatim, with no `ToArchiveRelativeStem` call and no +`ArchiveStemContract` call anywhere in the overload. The `MAPIFolder` overload (:336-357) does +normalize, at :345, and then delegates to the `string` overload at :346-352; `ToArchiveRelativeStem` +(:372-386) has exactly one caller. Any rooted value arriving directly at the `string` overload +therefore depends entirely on the boundary throw in `RequireArchiveRelativeStem`. + +### Why the two halves are one defect + +Issue #614 established the invariant, created `ArchiveStemContract`, enforced it at the boundary, +and enforced it on `SelectHierarchyPath` — but left the `SelectRow` producer and the `string` filing +overload unnormalized. The result is an asymmetry, not a user-visible fault: a rooted selection is +refused at the OK guard with a dialog. The asymmetry is nonetheless a live trap, demonstrated by +#614 remediation cycle 1, in which the OK guard was widened to agree with `SelectRow` and the +accepted value then threw at `RequireArchiveRelativeStem`. Closing the producer makes the guard and +the boundary agree by construction. + +### Corrections to the research file + +Two claims in `research/research.2026-08-29T12-30.md` did not survive re-verification. The tree +wins in both cases; this spec is written against the corrected facts. + +1. **Research §11 states that no dedicated `EfcDataModelTests.cs` exists and calls it a coverage + gap. That is wrong.** Both `QuickFiler.Test/Controllers/EfcDataModelTests.cs` (409 lines) and + `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` (123 lines) exist on this branch. The + latter is a dedicated `ToArchiveRelativeStem` suite added by issue #614 with **8** `[TestMethod]` + members (:21, :34, :48, :62, :72, :87, :100, :111), reaching the internal member through the + assembly's existing `InternalsVisibleTo("QuickFiler.Test")`. It is the natural home for the + change-B helper tests, and no new test infrastructure is required for change B. +2. **File-count arithmetic in two census headers.** Research §6 reports the `MoveToFolder` family as + "16 matching lines across 6 files"; the tree gives 16 lines across **5** files (the line count + agrees; the file count does not). Research §7 reports the `SelectedFolderPath` population as + "9 files: 2 production, 7 test"; the tree gives 9 files split **3 production / 6 test** (§7's own + later sentence, "both place all production occurrences in exactly 3 files", is the correct one). + Neither correction changes any conclusion; both are recorded so no downstream artifact inherits + the wrong figure. + +### Correction to the issue's third premise + +The `## Context` section above reproduces the issue narrative verbatim, including the claim that an +unhandled UI-thread exception arises because `ButtonOK_Click` is `async void` and rethrows. That +claim is inaccurate against the current tree; see the non-goals section for the verified evidence at +EfcFormController.cs:460-475 and the disposition in issue #695. + + +## Proposed Fix + +### Design summary (what changes where): + +Normalize at the producer, so that the OK guard and the filing boundary agree by construction +rather than by coincidence. + +- **A.** `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` — inside the existing + `IsFullOutlookPath` arm of `SelectRow`, bind the stem instead of discarding it into `out _`, + commit the stem when non-empty, and return without touching the selection when the stem is empty. +- **B.** `QuickFiler/Controllers/EfcDataModel.cs` — add one `internal static` pure helper and call + it from the `DestinationOlStem` assignment in the `string` overload of `MoveToFolderAsync`. +- **C.** `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` — correct one + assertion, one test name, and one arrange comment. +- **D.** `QuickFiler/Controllers/EfcSelectionGuard.cs` and + `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` — replace three stale deferral strings. + +Changes A and B are independent. A alone closes D1; B alone makes the second filing overload +self-consistent. Both are required to make "a rooted value cannot reach the filing boundary" true +of the whole chain rather than of one branch of it. + +### Boundaries and invariants to preserve: + +These must **not** change. Each is separately asserted in the acceptance criteria. + +1. **The no-bound-root pass-through mode (#439).** When `_boundRoot.Length == 0`, every value — + including a rooted one — is committed verbatim. In `SelectRow` this is the first conjunct at + Selection.cs:97; in `SelectHierarchyPath` it is the short-circuit at :111-115. Two existing tests + pin it and must pass unmodified. +2. **The `Trash to Delete` sentinel value.** `BreadcrumbRowBuilder.TrashRowText` must remain + byte-identical end to end. It is not `IsFullOutlookPath`, so it never enters the normalized arm + in change A; and it must be returned verbatim by the change-B helper, because + `EfcDataModel.MoveToFolderAsync` compares `folderpath != "Trash to Delete"` at :272 to decide + whether attachments are saved. Any mutation of that value silently flips attachment behavior. +3. **The provider lookup using the original rooted path.** `ToHierarchyPath` + (BreadcrumbBridgeRouter.cs:152-167) is not modified; the hierarchy provider continues to receive + the presented full path. The `provider.Verify(... ResolveLeafKeyAsync(fullTarget ...))` assertion + at `BreadcrumbBridgeRouterIssue439Tests.cs:161-164` stays exactly as written. +4. **The out-of-root rejection behavior.** A rooted target outside the bound root, and a + separator-boundary near miss such as `\Archive2\Clients`, are still rejected with the existing + value-free message `"Breadcrumb row rejected: target is outside the archive root."` and still + leave the prior selection untouched. +5. **Value-free diagnostics.** No new log message may embed the path or the archive root. The + existing test helper `AssertRejectionDiagnosticWithoutIdentifiers` + (BreadcrumbBridgeRouterIssue614Tests.cs:310-326) asserts that no message containing the queried + fragment contains `@`. +6. **Non-nulling rejection (#499).** A rejection is an early `return`. `SelectedFolderPath` is never + set to `null` by a rejected selection, and `SelectedFolderPathChanged` is not raised. Only the + clear-on-rebind path (BreadcrumbBridgeRouter.cs:143-146) nulls the property. +7. **`SelectHierarchyPath` is not modified.** Its four behaviors are the model for change A, not a + refactoring target. Its three call sites are unaffected. +8. **The guard/boundary contract is not relaxed.** `EfcSelectionGuard` still rejects rooted values + and `RequireArchiveRelativeStem` is still called at the boundary. This work removes the + producer's ability to emit a rooted value; it does not remove any backstop. +9. **No new throw site.** The change-B helper must not introduce an exception path that the current + verbatim assignment does not have. See "Error handling" below. + +### Dependencies or blocked work: + +- Depends on issue #614 (merged): `ArchiveStemContract`, the `SelectHierarchyPath` normalization, + and the boundary guard already exist. No new contract type is introduced. +- Issue #695 owns the `ArchiveRootPath` benign degrade, the keyboard entry points, the button-path + teardown, and the `OpenOlFolderAsync`/`OpenFsFolderAsync` assignments. This work must not + pre-empt those decisions. +- No external service, package, or release dependency. No new NuGet package. + +### Implementation strategy (what changes, not sequencing): + +#### Files/modules to change: + +| File | Change | Current size | Limit headroom | +|---|---|---|---| +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | A — normalize inside the rooted arm of `SelectRow` | 209 lines | 291 | +| `QuickFiler/Controllers/EfcDataModel.cs` | B — new `internal static` helper + call at the `DestinationOlStem` assignment | 424 lines | 76 | +| `QuickFiler/Controllers/EfcSelectionGuard.cs` | D — one XML-doc sentence | 79 lines | 421 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` | C — one assertion, one name, one comment (substitutions only) | 694 lines (already over the 500-line limit) | none — must not grow | +| `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` | D — two rationale strings | in-place substitution | n/a | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` | new file — router regression tests for A | new | 500 | +| `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` | new tests for the change-B helper | 123 lines | 377 | +| `QuickFiler.Test/QuickFiler.Test.csproj` | `` | — | — | + +`QuickFiler.Test.csproj` is a non-SDK-style net481 project with explicit `` items +(for example `Controllers\EfcDataModelIssue614Tests.cs` at :114 and +`Controllers\BreadcrumbBridgeRouterIssue614Tests.cs` at :62). A new test file that is not registered +there does not compile into the assembly and its tests silently never run. + +`EfcDataModel.cs` at 424 lines has 76 lines of headroom. The change-B helper plus its XML +documentation must fit inside that budget; if it does not, the helper moves to its own file rather +than the 500-line limit being exceeded. + +#### Functions/classes/CLI commands impacted: + +Selection family (Family A), the complete population — 2 declarations and 7 call sites, all +production, no interface declaration and no overload anywhere in the repository: + +| Kind | Location | Impacted by this fix | +|---|---|---| +| declaration | `BreadcrumbBridgeRouter.Selection.cs:83` `private void SelectRow(BreadcrumbRow row)` | **modified (A)** | +| declaration | `BreadcrumbBridgeRouter.Selection.cs:109` `private void SelectHierarchyPath(BreadcrumbRow, string)` | not modified | +| call | BreadcrumbBridgeRouter.cs:201 — `SelectFirstRow()` | behavior changes for a rooted at-or-under-root row 0 | +| call | BreadcrumbBridgeRouter.cs:286 — `ProcessInboundAsync`, `rowSelected` arm | same condition | +| call | BreadcrumbBridgeRouter.Arrows.cs:153 — `HandleUpArrow` | same condition | +| call | BreadcrumbBridgeRouter.Arrows.cs:161 — `MoveSelection` (Down arrow) | same condition | +| call | `BreadcrumbBridgeRouter.Selection.cs:33` — `ActivateSegment` → `SelectHierarchyPath` | no change | +| call | `BreadcrumbBridgeRouter.Selection.cs:47` — `ActivateChild` → `SelectHierarchyPath` | no change | +| call | BreadcrumbBridgeRouter.Arrows.cs:138 — `TryRightTreeTransitionAsync` (#440) → `SelectHierarchyPath` | no change | + +All four `SelectRow` call sites share one implementation, so the behavior change is uniform; there +is no per-call-site divergence to reason about. Two indirect entry points reach the family: +`_host.MessageReceived += OnHostMessageReceived` (BreadcrumbBridgeRouter.cs:55) and +`_router?.SelectFirstRow()` (EfcFormController.cs:438). + +`MoveToFolder` family, the complete population — 3 declarations across 2 declaring types and 6 call +sites: + +| Kind | Location | Impacted | +|---|---|---| +| declaration | `EfcDataModel.cs:259-265` — the `string` overload (M1) | **modified (B)** | +| declaration | `EfcDataModel.cs:336-343` — the `MAPIFolder` overload (M2) | not modified | +| declaration | EfcHomeController.ExecuteMoves.cs:89-95 — same-named forwarder / test seam, not an overload | not modified | +| call | EfcHomeController.ExecuteMoves.cs:78 | unchanged | +| call | EfcHomeController.ExecuteMoves.cs:98 | unchanged | +| call | `EfcDataModel.cs:346` — M2 delegating to M1 | unchanged; M2 still normalizes first via `ToArchiveRelativeStem` at :345 | +| call | EfcFormController.cs:537 | unchanged | +| call | EfcFormController.cs:844 | unchanged | +| call | QuickFiler.Test/Controllers/EfcHomeControllerExecuteMovesTests.cs:87 | unchanged | + +New member introduced: exactly one `internal static` helper on `EfcDataModel`. No public API is +added, removed, or changed anywhere in this work. + +#### Data flow and validation changes: + +Before (change A): `row.FilingTarget` → guard (rejects only out-of-root) → `CommitSelection(row, +selection)` → `SelectedFolderPath` = rooted value → `EfcFormController.SelectedFolder` → OK guard +refuses with a dialog. + +After (change A): `row.FilingTarget` → guard (rejects out-of-root, unchanged) → inside the rooted +arm, `TryMakeArchiveRelative(selection, _boundRoot, out stem)`; if `stem.Length == 0`, log and +return with no state mutation; otherwise `CommitSelection(row, stem)` → `SelectedFolderPath` = stem +→ OK guard accepts → boundary `RequireArchiveRelativeStem` accepts. + +Before (change B): `folderpath` → `DestinationOlStem = folderpath` (EfcDataModel.cs:287) → +`EmailFilerConfig.ResolvePaths` → `RequireArchiveRelativeStem` throws on a rooted value. + +After (change B): `folderpath` → helper; if not `IsFullOutlookPath`, returned verbatim +(byte-identical, covering relative stems and `"Trash to Delete"`); if `IsFullOutlookPath` and +`TryMakeArchiveRelative` succeeds with a non-empty stem, the stem is returned; in every other case +the input is returned unchanged so the existing boundary guard decides, exactly as it does today. + +No data migration, no persisted schema, no wire format, and no configuration key is affected. The +value flowing through is an in-process string. + +#### Error handling and logging updates: + +- **Change A** adds exactly one new diagnostic: a value-free `log.Error` on the archive-root-exact + non-selection, in the same shape as Selection.cs:124. It must embed neither the selection nor the + bound root. No new exception type, no new catch, no rethrow. +- **Change B** adds no exception path. The helper's contract is total: it returns a string for every + input and never throws. +- **`ToArchiveRelativeStem` versus the change-B helper — the semantic choice, stated explicitly.** + The two available semantics differ on the archive-root-exact input. + `ArchiveStemContract.TryMakeArchiveRelative` returns `true` with an empty stem for that input + (ArchiveStemContract.cs:124-127), whereas `EfcDataModel.ToArchiveRelativeStem` **throws** + `ArgumentException` for it, because `RequireArchiveRelativeStem(stem, ...)` at + `EfcDataModel.cs:384` rejects an empty value. That throw is pinned by + `ToArchiveRelativeStem_ArchiveRootItself_Throws` + (`QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs:62-69`). + + **The change-B helper adopts neither throw semantics: it is total and never throws.** For the + archive-root-exact input, and for any rooted input that `TryMakeArchiveRelative` rejects, it + returns the input verbatim and lets `RequireArchiveRelativeStem` at the filing boundary decide, + which is precisely what happens today. Rationale: + + 1. **Non-regression.** A throwing helper would convert an input that currently produces a + redacted `ArgumentException` at the boundary into an `ArgumentException` raised earlier, from a + different frame, on a call chain whose only handler is the button-path + `BoundaryErrorSink`; the keyboard entry points have no handler at all. Change B would then have + altered failure behavior on a path issue #695 explicitly owns. + 2. **Scope.** Deciding what an aborted filing operation should look like to the user is issue + #695's work item, not this one. + 3. **Sufficiency.** Change B's purpose is to stop a *normalizable* rooted value from reaching the + boundary unnormalized. Non-normalizable values are already handled correctly by the backstop. + 4. **Asymmetry with `ToArchiveRelativeStem` is correct.** That method's input is always a full + Outlook `MAPIFolder.FolderPath` supplied by the create paths, so an unconditional throw is + right there. The `string` overload's input is a *presented selection* that is normally already + relative and may legitimately be the `"Trash to Delete"` sentinel; an unconditional + `ToArchiveRelativeStem` call at :287 would throw on every ordinary filing operation. + + Net effect: the helper can only convert a value the boundary would have rejected into one it + accepts. It can never turn an accepted value into a rejected one, and it never adds a throw. + +#### Rollback/feature-flag considerations (if applicable): + +No feature flag. The change is a small, self-contained behavior correction whose rollback is a +revert of the branch. A flag would reintroduce the exact producer/guard asymmetry this issue exists +to remove. + +### Technical specifications (interfaces/contracts): + +#### Inputs/outputs and formats: + +- `SelectRow(BreadcrumbRow row)` — unchanged signature, `private void`. Input: the row whose + `FilingTarget` (or `BreadcrumbRowBuilder.TrashRowText` for the trash pseudo-row) is the candidate + selection. Output: a mutation of `SelectedFolderPath` plus a render post and a + `SelectedFolderPathChanged` event, or no observable effect at all. +- The change-B helper — `internal static string`, two `string` parameters (the candidate path and + the archive ancestor), total function, no side effects, no I/O, no logging. Returns the value to + assign to `DestinationOlStem`. +- `SelectedFolderPath` — `public string? { get; private set; }` (BreadcrumbBridgeRouter.cs:59). + Type unchanged. The **value domain narrows**: after this work the only values the producer can + emit are an archive-relative stem, the `Trash to Delete` sentinel, `null` (clear-on-rebind), or — + in no-bound-root mode only — the presented value verbatim. + +#### Required configuration keys and defaults: + +None. No new configuration key, app setting, or default is introduced. The archive root continues to +be supplied to the router as the fourth argument of the internal `BindRowsAsync` overload +(BreadcrumbBridgeRouter.cs:92-97), which normalizes it with `TrimEnd('\\','/')` at :107-109 — +so a separator-only value such as `@"\"` yields `_boundRoot.Length == 0` and selects the +pass-through mode. + +#### Backward-compatibility expectations: + +- No public API signature changes. `SelectedFolderPath`'s setter is `private`, so no external caller + can be broken by the narrowed value domain. +- The single cross-type consumer is `EfcFormController.SelectedFolder` (EfcFormController.cs:321), + and every downstream reader either improves or is unaffected — see Data / API / Config Impact. +- Persisted artifacts: the QuickFile metrics CSV column (EfcHomeController.Metrics.cs:56) will carry + a stem rather than a rooted path for the affected case. There are no in-repo readers of that CSV, + and a stem is strictly less identifying than a rooted store path. + +#### Performance constraints (latency/throughput/memory): + +None beyond the existing budget. Change A binds an out-parameter that is already being computed and +discarded — no additional call, no additional allocation beyond the substring +`TryMakeArchiveRelative` already produces. Change B adds one predicate call and at most one +substring per filing operation, on a path that already awaits Outlook I/O. Both are on user-gesture +paths measured in single-digit microseconds. + +## Assumptions, Constraints, Dependencies + +- **Assumptions (environment, data, access):** + - Windows 11, .NET Framework 4.8.1 VSTO add-in; `net481` test projects. + - The archive root supplied to `BindRowsAsync` is a full Outlook path, and `_boundRoot` has + already had trailing separators trimmed (BreadcrumbBridgeRouter.cs:107-109). + - Path comparison is `OrdinalIgnoreCase` throughout `ArchiveStemContract`; both `\` and `/` are + accepted as separators. + - `QuickFiler.Test` can reach `internal` members of `QuickFiler` through the existing + `InternalsVisibleTo("QuickFiler.Test")`; no assembly-attribute change is needed. + - No Outlook process is available in the test environment; every new test must be headless. + +- **Constraints (budget, performance, compatibility):** + - No production, test, or reusable script file may exceed **500 lines**. + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` is already 694 lines and + must not grow; `QuickFiler/Controllers/EfcDataModel.cs` is 424 lines and has 76 lines of + headroom. + - Nullable enforcement is per-file opt-in via `#nullable enable`. + `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` carries it at line 1, so `CS86xx` + diagnostics become build errors there under `/p:TreatWarningsAsErrors=true`; + `QuickFiler/Controllers/EfcDataModel.cs` does **not** carry it and does not participate in + nullable analysis. `SelectedFolderPath` is `string?` while `CommitSelection` takes a + non-nullable `string`, so no nullable temporary may be introduced in change A. + - Toolchain order is fixed and restarts from step 1 on any failure or file change: + 1. `dotnet tool run csharpier format .` (verify with `dotnet tool run csharpier check .`) + 2. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + 3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + 4. `vstest.console.exe /EnableCodeCoverage` + + `/p:Nullable=enable` must not be added and `/t:Build` must not be substituted for `/t:Rebuild`. + - Tests are MSTest with Moq and FluentAssertions. No temporary files, no wall-clock waits, no + external dependencies. + - Coverage: repository line coverage must stay at or above the policy floor, changed lines must + not lose coverage, and the new helper must reach the new-code coverage target. + +- **External dependencies (services, libraries, releases):** none. No new package. Existing test + package versions in `QuickFiler.Test/packages.config` (FluentAssertions 8.10.0, Moq 4.20.72, + MSTest 4.3.3, all `net481`) are sufficient. + +## Data / API / Config Impact + +- **User-facing or API changes:** + + No public API signature changes. One `internal static` helper is added to `EfcDataModel`. The + user-visible change is that selecting a breadcrumb row whose filing target is a rooted path at or + under the bound archive root now files successfully instead of being refused at OK with "Please + select a valid folder." + + **Blast radius of `SelectedFolderPath` changing from a rooted value to a stem.** The complete + production surface is 9 lines across 3 files: 2 write sites + (`BreadcrumbBridgeRouter.Selection.cs:134` in `CommitSelection`, and BreadcrumbBridgeRouter.cs:145 + the #499 clear-on-rebind), 3 read sites (BreadcrumbBridgeRouter.cs:143 the #499 change-detection + guard, `BreadcrumbBridgeRouter.Selection.cs:138` the event payload, and EfcFormController.cs:321 + the sole cross-type read), 1 property declaration (BreadcrumbBridgeRouter.cs:59), 1 doc reference + (:61), and 2 event-only lines (:62, :146). Only the `CommitSelection` write is reached by + `SelectRow`. + + Downstream consumers through `EfcFormController.SelectedFolder`, classified (research §7): + + | Consumer | Classification | + |---|---| + | `EfcFormController.IsValidSelection` → `EfcSelectionGuard.IsValidCreationSelection` | **Improves.** A rooted value is rejected at EfcSelectionGuard.cs:76, so the New-Folder gesture currently reports "Please select a valid folder" even for an in-archive folder. A stem passes. | + | `EfcFormController.ActionOkAsync` → `EfcSelectionGuard.IsValidFilingSelection` | **Improves.** Rooted values are rejected at EfcSelectionGuard.cs:50, so a rooted-under-root breadcrumb selection is refused at OK today. A stem passes the guard and files. | + | `ButtonCreateClickAsync` / `CreateFolderAsync` → `FolderHelper.CreateFolder(Async)` | **Improves.** These concatenate the selection beneath the archive root, so a stem is the correct input and a rooted value was never valid. | + | `ActionOkAsync` → `OpenOlFolderAsync` / `OpenFsFolderAsync` → `DestinationOlStem` → `ResolvePaths` | **Improves.** A rooted value throws at `RequireArchiveRelativeStem`; a stem does not. (Their own verbatim assignments remain a non-goal, issue #695.) | + | `EfcHomeController.ExecuteMovesCoreAsync` → the `string` overload → `DestinationOlStem` → `ResolvePaths` | **Improves.** This is the D1 leak the fix closes. | + | `EfcHomeController.HandleMoveResult` failure text | Cosmetic improvement. The message embeds the value; a stem is strictly less identifying than a rooted store path. | + | `EfcHomeController.QuickFileMetrics_WRITE` CSV column | Cosmetic. Zero in-repo readers. | + | `EfcDataModel` trash-sentinel comparison at :272 | **No change.** `Trash to Delete` is not `IsFullOutlookPath`. | + | Every consumer reached with an empty `_boundRoot` | **No change.** The pass-through mode is untouched. | + | The #499 clear-on-rebind pair (BreadcrumbBridgeRouter.cs:143-145) | **No change.** Writes `null`, reads for null-ness only. | + | `EfcItemController.SelectedFolder` | **No change.** Reads `_itemViewer.GetSelectedFolder()`, a different source. | + | The Family-B breadcrumb surface | **No change.** It has no `SelectedFolderPath` member. | + + Summary of the classification: **every consumer that observes a change improves**, because a + rooted selection is currently refused by the selection guard with a dialog. No consumer requires a + rooted value. The single direction that could be called a behavior loss is the archive-root-exact + case becoming a deterministic non-selection: today it produces a rooted value that + `IsValidFilingSelection` rejects with a dialog; afterwards it produces no selection at all and the + prior selection survives. Both outcomes refuse to file the archive root; the new one is quieter + and matches `SelectHierarchyPath`. + +- **Data or migration considerations:** none. No persisted state, schema, or stored path is + affected. The QuickFile metrics CSV gains a shorter value in one column; it has no in-repo + readers and no schema contract. + +- **Logging/telemetry updates (if any):** one new value-free `log.Error` diagnostic for the + archive-root-exact non-selection in `SelectRow`. No new logger, appender, or category. No message + may embed a path, mailbox address, or the archive root. + +- **Compatibility notes (CLI flags, config schemas, versioning):** none. No CLI surface, no config + schema, no versioned contract. `QuickFiler.Test.csproj` gains one `` item for the + new test file. + +## Test Strategy + +Framework: **MSTest** + **Moq** + **FluentAssertions** (`QuickFiler.Test`, `net481`). No temporary +files, no Outlook process, no wall-clock waits. + +Seeded from the issue, with dispositions (these are inputs, not acceptance criteria; the +authoritative criteria are under `## Acceptance Criteria` below): + +- Normalize in `SelectRow` — **in scope, change A.** +- Wire normalization into the `string` overload of `MoveToFolderAsync` — **in scope, change B**, via + a gated pure helper rather than a direct `ToArchiveRelativeStem` call. +- Benign degrade for the OK-path read of `ArchiveRootPath` — **out of scope; issue #695**, and its + premise is corrected under Root Cause Analysis. +- Update the issue #439 rooted-target test to assert the stem — **in scope, change C**, recorded as + a deliberate spec correction. +- Unit coverage areas: `SelectRow` for rooted under-root, rooted root-exact, rooted out-of-root, + relative, and empty-bound-root inputs, plus the `string` overload's normalization — **in scope**; + the unresolvable-archive-root degrade — out of scope. +- The #614 remediation composition test must still pass — **in scope as a preserved test.** +- Manual verification of the archive-root row — **in scope.** + +### Fixture patterns to reuse (do not invent a new one) + +Two established shapes exist; new tests must use one of them. + +- **Shape 1 — per-test local construction.** Strict `Mock` and + `Mock` built inline, bound through the **internal 4-argument** `BindRowsAsync` + overload whose fourth positional argument is the archive root. Reference implementation: + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs:123-158`, with local helpers + `Key(string)` (:668-671), `Chain(string, string, string)` (:673-687), and + `Segment(string, string, bool)` (:689-692). +- **Shape 2 — `[TestInitialize]` fixture with a `BindChain` helper and a log4net `MemoryAppender`.** + Required whenever a test asserts the rejection *diagnostic* rather than only the selection value. + Reference implementation: BreadcrumbBridgeRouterIssue614Tests.cs — `Setup()` :38-52, `Cleanup()` + :54-58, `BindStandardChain()` :224-234, `BindChain(...)` :236-262 (which passes `ArchiveRoot` as + the fourth `BindRowsAsync` argument at :257), `Inbound(json)` :264-267, `RowSelected(int)` + :288-291, `RenderedMessages()` :304-307, `AssertRejectionDiagnosticWithoutIdentifiers(fragment)` + :310-326, appender attach/detach :338-356. + +The `internal` 4-argument `BindRowsAsync` overload is already visible to `QuickFiler.Test`, so no +`InternalsVisibleTo` change is required. To produce `_boundRoot.Length == 0` in a test, pass +`string.Empty`, `null`, whitespace, **or a separator-only value** such as `@"\"` — +`BindRowsAsync` applies `TrimEnd('\\','/')` at BreadcrumbBridgeRouter.cs:107-109, which is exactly +what `BreadcrumbBridgeRouterIssue439Tests.cs:645` relies on. + +### Destination files (500-line limit respected) + +Verified current sizes: `BreadcrumbBridgeRouterIssue439Tests.cs` **694 lines** (already over the +limit), BreadcrumbBridgeRouterIssue614Tests.cs 358, `EfcDataModelIssue614Tests.cs` 123, +EfcDataModelTests.cs 409, `BreadcrumbBridgeRouter.Selection.cs` 209, `EfcDataModel.cs` 424. + +| Tests | Destination | Reason | +|---|---|---| +| New change-A router regression tests | **`QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` (new file, Shape 2)** | `BreadcrumbBridgeRouterIssue439Tests.cs` is 694 lines and must not grow; BreadcrumbBridgeRouterIssue614Tests.cs at 358 lines cannot absorb a full new suite plus fixture without approaching the limit. The new file must be registered in `QuickFiler.Test/QuickFiler.Test.csproj` with a `` item. | +| New change-B helper tests | **`QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`** | It is the existing dedicated suite for the sibling normalizer `ToArchiveRelativeStem`, has 377 lines of headroom, and already reaches the internal member through `InternalsVisibleTo("QuickFiler.Test")`. Its 8 existing test methods stay unchanged. | +| Corrected assertion (change C) | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` | Substitution only — one assertion, one method name, one comment. Net line count must not increase. | +| Stale rationale strings (change D) | `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` | Substitution only. | + +### Regression tests to add or update + +**Change A — `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` (new):** + +1. `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected` — bind a row whose `FilingTarget` equals + the bound archive root; assert `SelectedFolderPath` is unchanged from its prior value, that no + `SelectedFolderPathChanged` event is raised, and that a value-free rejection diagnostic was + logged (`AssertRejectionDiagnosticWithoutIdentifiers`). +2. `RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection` — establish a valid stem + selection first, then select the archive-root row; assert the prior stem survives and is never + nulled (#499). +3. `RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` — assert + `SelectedFolderPath` is the stem, not the rooted input. +4. `RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem` + — root with differing case and a trailing separator; assert the same stem. +5. `RowSelected_RelativeFilingTarget_CommitsTheValueVerbatim` — assert byte-identical commit of an + ordinary relative suggestion target (the regression a method-wide rewrite would cause). +6. `RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim` — assert the committed value is exactly + `Trash to Delete`. +7. `RowSelected_OutOfRootRootedTarget_IsStillRejected` and + `RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected` (`\Archive2\Clients` against + `\Archive`) — assert the pre-existing rejection message and no state mutation. +8. `RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim` — bind with `@"\"`; + assert the rooted value is committed verbatim. +9. `SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` — proves the + normalization is on the shared implementation and therefore reaches all four `SelectRow` call + sites, not only the `rowSelected` inbound message. + +**Change B — `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` (added to the existing +class or a sibling class in the same file):** + +1. Rooted path strictly under the ancestor returns the stem. +2. Rooted path under an ancestor differing in case returns the stem. +3. Relative stem (`Clients\North`) is returned **verbatim and byte-identical**. +4. `"Trash to Delete"` is returned verbatim and byte-identical. +5. Archive-root-exact input is returned verbatim and **does not throw** (the explicit divergence + from `ToArchiveRelativeStem_ArchiveRootItself_Throws`). +6. Out-of-root / cross-store rooted input is returned verbatim and does not throw. +7. Null, empty, and whitespace ancestor, and a separator-only ancestor, return the input verbatim + and do not throw. +8. Null or empty candidate path returns the input verbatim and does not throw. + +**Change C — corrected assertion:** in +`QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, line 165 becomes +`router.SelectedFolderPath.Should().Be(@"Clients\North");`. Derivation of the expected value: with +`archiveRoot = @"\Archive"` (:123) and `fullTarget = @"\aRcHiVe\Clients\North"` (:124), +`TryMakeArchiveRelative` matches through `StartsWith(OrdinalIgnoreCase)` (ArchiveStemContract.cs:131), +the boundary character at index 8 is `\` (:137-141), and +`stem = fullTarget.Substring(8).TrimStart('\\','/')` = `Clients\North`. The method is renamed so it +no longer claims the target "RemainsUnchanged", and the arrange comment at :121-122 is narrowed to +the provider claim it still supports. The `provider.Verify(...)` assertion at :161-164 is preserved +verbatim. + +### Existing tests that must pass unmodified + +- The two no-bound-root pass-through assertions: + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs:665` + (`Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection`, bound with `@"\"` at :645) and + BreadcrumbBridgeRouterIssue614Tests.cs:221 + (`SegmentActivate_WithNoBoundArchiveRoot_PreservesThePassThroughMode`, 3-argument bind at :213). + Both reach `SelectHierarchyPath` with an empty `_boundRoot` and are unaffected. +- `RowSelected_OutOfRootFilingTarget_DoesNotStoreAFullOutlookPath` + (BreadcrumbBridgeRouterIssue614Tests.cs:169). +- The composition test `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` + (`QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:167-213`), which filters a 10-element + candidate matrix through `IsValidFilingSelection` and asserts `EmailFilerConfig.ResolvePaths()` + does not throw for anything the guard accepts. It exercises no router, so it is structurally + untouched; the fix makes it more meaningful, because the producer now emits only values in the + accepted class. +- The 8 `ToArchiveRelativeStem` tests in `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, + including `ToArchiveRelativeStem_ArchiveRootItself_Throws` (:62-69). +- All `ArchiveStemContractTests` in `UtilitiesCS.Test`. + +### Edge cases and negative scenarios + +Covered by the lists above: archive-root-exact (`TryMakeArchiveRelative` returns `true` with an +empty stem); trailing-separator and differing-case roots; forward-slash boundary; the `Archive2` +separator-boundary near miss; out-of-root and cross-store values; already-relative values; the +trash sentinel; null / empty / whitespace / separator-only archive root; null and empty candidate +path; and a prior valid selection surviving a rejected one. + +### Error handling and logging verification + +- The new archive-root-exact diagnostic must be asserted **value-free**: reuse + `AssertRejectionDiagnosticWithoutIdentifiers`, which requires that no message containing the + queried fragment contains `@`. +- Assert that a rejection raises no `SelectedFolderPathChanged` event and performs no write. +- Assert that the change-B helper never throws for any input in the matrix above, including inputs + for which `ToArchiveRelativeStem` throws. + +### Coverage impact and targets for changed lines/modules + +- The change-B helper is a new method and must meet the new-code coverage target, with every branch + of its `IsFullOutlookPath` / `TryMakeArchiveRelative` gate exercised. +- Change A adds one branch inside an existing method; both its outcomes (empty stem, non-empty stem) + are covered by the new tests. +- Repository line coverage must not fall below the policy floor, and no changed line may lose + coverage relative to the base commit. +- Coverage evidence is produced by step 4 of the toolchain and written under the canonical evidence + kinds defined by `evidence-and-timestamp-conventions`: the pre-change capture under + `/evidence/baseline/` and the post-change capture under `/evidence/qa-gates/`. + `/evidence/coverage/` is not a canonical evidence kind and must not be used. + +### Toolchain commands to run (format → lint → type-check → test) + +1. `dotnet tool run csharpier format .` (verify: `dotnet tool run csharpier check .`) +2. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` +3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` +4. `vstest.console.exe /EnableCodeCoverage` + +Restart from step 1 on any failure or any file the tools change. Do not add `/p:Nullable=enable`; +do not substitute `/t:Build` for `/t:Rebuild`. + +### Manual validation steps (if required) + +1. Bind breadcrumb rows with a real archive root and present a suggestion row whose filing target is + the archive root itself. Select it. Confirm it is a deterministic non-selection: the prior + selection is still shown, no error dialog appears, and a value-free entry is written to the log. +2. Present a suggestion row whose filing target is rooted and strictly under the archive root. + Select it, then press OK. Confirm the item files successfully rather than being refused with + "Please select a valid folder." +3. Select the `Trash to Delete` pseudo-row and confirm the trash behavior, including attachment + handling, is unchanged. + + +## Acceptance Criteria + +Every number appearing below was re-derived against the working tree while authoring this spec, +using a search strategy independently constructed from the one in the research file, and the two +results agreed. Where they did not agree, the tree value is used and the disagreement is recorded +under "Corrections to the research file". + +### Change A — producer normalization in `SelectRow` + +- [ ] AC1. Selecting a row whose `FilingTarget` is a full Outlook path **exactly equal** to the + bound archive root is a deterministic non-selection: `SelectRow` returns early, + `SelectedFolderPath` is not written, `SelectedFolderPathChanged` is not raised, and any prior + valid selection survives unchanged. Verified by a named test in + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`. +- [ ] AC2. Selecting a row whose `FilingTarget` is a full Outlook path **strictly under** the bound + archive root commits the archive-relative stem, not the rooted input — including when the + root differs in case and when the root carries a trailing separator. Verified by named tests + in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`. +- [ ] AC3. The new behavior is nested inside the existing + `ArchiveStemContract.IsFullOutlookPath(selection)` arm of the guard in + `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`. An ordinary **relative** + suggestion target is still committed byte-identically, proven by a test that would fail under + a method-wide "commit only when `TryMakeArchiveRelative` succeeds" rewrite. +- [ ] AC4. The no-bound-root pass-through mode is preserved: with `_boundRoot.Length == 0` every + value, including a rooted one, is committed verbatim. The **2** existing tests that pin this + pass through unmodified — `BreadcrumbBridgeRouterIssue439Tests.cs:665` (bound with `@"\"` at + :645) and BreadcrumbBridgeRouterIssue614Tests.cs:221 (3-argument bind at :213) — and a new + test covers the `SelectRow` path in the same mode. +- [ ] AC5. The `Trash to Delete` pseudo-row still commits `BreadcrumbRowBuilder.TrashRowText` + byte-identically, and `EfcDataModel.MoveToFolderAsync`'s `folderpath != "Trash to Delete"` + comparison at `EfcDataModel.cs:272` continues to take the same branch as before. +- [ ] AC6. Out-of-root rejection is unchanged: a rooted target outside the bound root, and a + separator-boundary near miss such as `\Archive2\Clients`, are still rejected with the existing + message `"Breadcrumb row rejected: target is outside the archive root."`, leave the prior + selection untouched, and raise no event. + `RowSelected_OutOfRootFilingTarget_DoesNotStoreAFullOutlookPath` + (BreadcrumbBridgeRouterIssue614Tests.cs:169) passes unmodified. +- [ ] AC7. The archive-root-exact non-selection emits a **value-free** diagnostic that embeds + neither the selection nor the archive root, asserted with the existing + `AssertRejectionDiagnosticWithoutIdentifiers` helper shape (no message containing the queried + fragment contains `@`). +- [ ] AC8. `SelectHierarchyPath` (`BreadcrumbBridgeRouter.Selection.cs:109-129`) and + `CommitSelection` (:131-139) are not modified. +- [ ] AC9. The selection family is unchanged in shape: still exactly **2** declarations + (`BreadcrumbBridgeRouter.Selection.cs:83` and `:109`) and **7** call sites (4 to `SelectRow` + at BreadcrumbBridgeRouter.cs:201, :286 and BreadcrumbBridgeRouter.Arrows.cs:153, :161; 3 to + `SelectHierarchyPath` at `BreadcrumbBridgeRouter.Selection.cs:33`, `:47` and + BreadcrumbBridgeRouter.Arrows.cs:138). No new declaration, no overload, no interface member, + and no new call site is introduced, and no member of the unrelated Family-B `SelectRow(int)` + surface is touched. +- [ ] AC10. The normalization is on the shared implementation, so all four `SelectRow` call sites + observe it. Proven by at least one regression test that reaches `SelectRow` through + `SelectFirstRow()` rather than through the `rowSelected` inbound message. + +### Change B — normalization in the `string` overload of `MoveToFolderAsync` + +- [ ] AC11. `QuickFiler/Controllers/EfcDataModel.cs` declares exactly one new `internal static` + helper that takes the candidate path and the archive ancestor and returns the value to assign, + and the `DestinationOlStem` assignment in the `string` overload (currently + `EfcDataModel.cs:287`) calls it. The helper is pure: no I/O, no logging, no static mutable + state, and it is invoked directly by unit tests without constructing an `EmailFiler`. +- [ ] AC12. The helper is gated on `ArchiveStemContract.IsFullOutlookPath`. Any value that is not a + full Outlook path — every ordinary relative stem and the `"Trash to Delete"` sentinel — is + returned **verbatim and byte-identical**, asserted by named tests. +- [ ] AC13. For a rooted value at or strictly under the ancestor, the helper returns the + archive-relative stem. +- [ ] AC14. The helper is **total and never throws**, for any input including archive-root-exact, + out-of-root, cross-store, null/empty candidate, and null/empty/whitespace/separator-only + ancestor; in each of those cases it returns the input verbatim so the existing boundary guard + decides exactly as it does today. This is a deliberate divergence from + `EfcDataModel.ToArchiveRelativeStem`, which throws on the archive-root-exact input; the + rationale is recorded under "Error handling and logging updates". +- [ ] AC15. `EfcDataModel.ToArchiveRelativeStem` (`EfcDataModel.cs:372-386`), the `MAPIFolder` + overload (`:336-357`), and its call to `ToArchiveRelativeStem` at `:345` are unmodified, and + the **8** existing `ToArchiveRelativeStem` tests in + `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` (methods at :21, :34, :48, :62, + :72, :87, :100, :111) pass unchanged — including + `ToArchiveRelativeStem_ArchiveRootItself_Throws`. +- [ ] AC16. The `MoveToFolder` family is unchanged in shape apart from the helper call: still + **3** declarations (`EfcDataModel.cs:259`, `EfcDataModel.cs:336`, and the same-named forwarder + at EfcHomeController.ExecuteMoves.cs:89) and **6** call sites (EfcHomeController.ExecuteMoves.cs:78 + and :98, `EfcDataModel.cs:346`, EfcFormController.cs:537 and :844, and + QuickFiler.Test/Controllers/EfcHomeControllerExecuteMovesTests.cs:87). No new overload and no + signature change. +- [ ] AC17. `EfcDataModel.OpenOlFolderAsync` (`:299-316`) and `OpenFsFolderAsync` (`:318-334`) are + **not** modified, and no `Globals.Ol.ArchiveRootPath` read anywhere gains a new try/catch or + degrade — those are non-goals owned by issue #695. + +### Change C — test spec correction + +- [ ] AC18. `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs:165` asserts + `router.SelectedFolderPath.Should().Be(@"Clients\North");`, the enclosing test method is + renamed so it no longer asserts that a rooted target "RemainsUnchanged", and the arrange + comment at :121-122 is narrowed to the provider claim it still supports. +- [ ] AC19. The companion assertion at `BreadcrumbBridgeRouterIssue439Tests.cs:161-164` + (`provider.Verify(p => p.ResolveLeafKeyAsync(fullTarget, ...), Times.Once)`) is preserved + verbatim, and `ToHierarchyPath` (BreadcrumbBridgeRouter.cs:152-167) is unmodified, so the + provider lookup still uses the original rooted path. +- [ ] AC20. Exactly **1** existing test assertion changes its expected value across the entire + repository — the one at `BreadcrumbBridgeRouterIssue439Tests.cs:165`. No other existing + assertion in any test project is modified, weakened, disabled, or deleted. +- [ ] AC21. The change is recorded in the change description as a **deliberate spec correction**: + the issue #439 criterion that a rooted target survives selection is superseded by issue #614's + archive-relative-stem invariant, which #614 enforced on the `SelectHierarchyPath` half and at + the filing boundary but not on the `SelectRow` half. It is explicitly not a weakened test. + +### Change D — stale-comment cleanup + +- [ ] AC22. All **3** stale deferral records are corrected to state that producer-side normalization + is implemented: `QuickFiler/Controllers/EfcSelectionGuard.cs:30`, + `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146`, and the `because` string at + `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:152`. After the change, a repository + grep for `deferred to issue #637` across `*.cs` returns **0** matches. +- [ ] AC23. `EfcSelectionGuard` behavior is unchanged — `IsValidFilingSelection` and + `IsValidCreationSelection` still reject rooted values — and every test in + `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` passes, including the composition test + `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` (:167-213). + +### Cross-cutting + +- [ ] AC24. The `SelectedFolderPath` production surface is unchanged in shape: still **9** lines + across **3** files, with **2** write sites (`BreadcrumbBridgeRouter.Selection.cs:134`, + BreadcrumbBridgeRouter.cs:145) and **3** read sites (BreadcrumbBridgeRouter.cs:143, + `BreadcrumbBridgeRouter.Selection.cs:138`, EfcFormController.cs:321). No new write site, no + new public API member, and the property's `private set` is preserved. +- [ ] AC25. File-size limits hold. `QuickFiler/Controllers/EfcDataModel.cs` remains at or under 500 + lines (424 before the change); + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` does not grow beyond its + current 694 lines; and every new or modified file, including + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` and + `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, is at or under 500 lines. +- [ ] AC26. `QuickFiler.Test/QuickFiler.Test.csproj` contains a `` item for + `Controllers\BreadcrumbBridgeRouterIssue637Tests.cs`, and the new tests are observed executing + in the vstest run output (a test file absent from this non-SDK project compiles into nothing + and silently never runs). +- [ ] AC27. Nullable posture is respected: `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` + keeps its `#nullable enable` directive, the edited lines introduce no `CS86xx` diagnostic under + `/p:TreatWarningsAsErrors=true`, and no nullable temporary is passed to `CommitSelection`, + whose parameter is a non-nullable `string`. +- [ ] AC28. Full C# toolchain pass completed in order with no failures in the final pass, using + exactly these commands: `dotnet tool run csharpier format .` (verified with + `dotnet tool run csharpier check .`); + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`; + `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`; + `vstest.console.exe /EnableCodeCoverage`. `/p:Nullable=enable` was not + added and `/t:Build` was not substituted. +- [ ] AC29. Coverage evidence is captured under the canonical evidence kinds — the pre-change + capture under `/evidence/baseline/` and the post-change capture under + `/evidence/qa-gates/`, per `evidence-and-timestamp-conventions`: repository line + coverage is at or above the policy floor, no changed line loses coverage relative to the base + commit, and the new change-B helper meets the new-code coverage target with both sides of its + gate exercised. +- [ ] AC30. No behavior outside changes A-D is altered. Specifically unchanged: + `ArchiveStemContract`, `EmailFilerConfig.ResolvePaths` / `RequireArchiveRelativeStem`, + `EfcHomeController.ExecuteMovesAsync`, EfcFormController.cs, the Family-B breadcrumb surface, + and all `UtilitiesCS.Test` suites. + +## Risks & Mitigations + +- **Technical or operational risks:** + 1. *Over-broad normalization in `SelectRow`.* Applying the new rule outside the + `IsFullOutlookPath` arm would reject every ordinary relative suggestion target and the trash + pseudo-row, because `TryMakeArchiveRelative` returns `false` for both. This is the single + highest-severity failure mode of change A. + 2. *A throwing change-B helper.* Adopting `ToArchiveRelativeStem`'s throw semantics would add a + new exception path on the OK filing chain, whose only handler is the button-path + `BoundaryErrorSink` and whose keyboard entry points have none — pre-empting issue #695. + 3. *Silent mutation of the trash sentinel.* Any normalization applied upstream of the + `folderpath != "Trash to Delete"` comparison at `EfcDataModel.cs:272` would silently flip + attachment-saving behavior. + 4. *Perception of a weakened test.* Change C changes an assertion that a prior issue deliberately + added; without the recorded rationale a reviewer could read it as coverage loss. + 5. *A new test file that never runs.* `QuickFiler.Test.csproj` is a non-SDK project with explicit + `` items; an unregistered file compiles into nothing and reports no failures. + 6. *Test file line-limit pressure.* The most natural home for the change-A tests is already 694 + lines, over the 500-line limit. + +- **Mitigations and rollbacks:** + 1. AC3 requires a byte-identical relative-target commit test that fails under the over-broad + rewrite; AC5 requires the same for the trash sentinel. + 2. AC14 fixes the helper's total, non-throwing contract and requires tests for every input class, + including those on which `ToArchiveRelativeStem` throws. + 3. AC5 pins the sentinel through both the router and the filing comparison. + 4. AC21 requires the spec correction to be recorded explicitly in the change description, with the + superseding-invariant rationale. + 5. AC26 requires both the `` item and observed execution in the vstest output. + 6. AC25 pins the current sizes and routes new router tests to a new file. + - Rollback is a revert of the branch. No feature flag, no data migration, and no persisted state + means a revert restores the prior behavior exactly. + +## Rollout & Follow-up + +- **Release/rollout steps:** ordinary branch → PR → merge. The change ships inside the VSTO add-in + build; there is no separate deployment step, configuration change, or migration. No user + communication is required beyond the change description, since the observable effect is that a + previously refused in-archive selection now files. +- **Post-fix monitoring or clean-up tasks:** + - Confirm after merge that no log entry from the new archive-root-exact diagnostic contains a path + or mailbox address. + - Issue #695 remains open for the `ArchiveRootPath` benign degrade, the two uncaught keyboard + entry points, the half-completed button-path teardown, and the verbatim `DestinationOlStem` + assignments in `OpenOlFolderAsync` / `OpenFsFolderAsync`. + - Once #695 lands, re-evaluate whether the change-B helper should become the single normalization + funnel for all three `EmailFilerConfig` construction sites in `EfcDataModel`. +- **Links:** + - Issue: https://github.com/drmoisan/TaskMaster/issues/637 + - Parent invariant: issue #614 (`ArchiveStemContract`, the boundary guard, and the + `SelectHierarchyPath` normalization), plus its remediation cycle 1 revert. + - Prior producer spec: issue #439 (rooted-target pass-through), superseded in part by change C. + - Follow-up: issue #695 (excluded surfaces and the OK-chain error handling). + - Research: `research/research.2026-08-29T12-30.md`. diff --git a/docs/features/potential/promoted/2026-08-29-efc-ok-chain-unhandled-keyboard-paths-and-half-completed-teardown.md b/docs/features/potential/promoted/2026-08-29-efc-ok-chain-unhandled-keyboard-paths-and-half-completed-teardown.md new file mode 100644 index 000000000..b98ea7b6c --- /dev/null +++ b/docs/features/potential/promoted/2026-08-29-efc-ok-chain-unhandled-keyboard-paths-and-half-completed-teardown.md @@ -0,0 +1,135 @@ +# efc-ok-chain-unhandled-keyboard-paths-and-half-completed-teardown (Issue #695) + +- Date captured: 2026-08-29 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/efc-ok-chain-unhandled-keyboard-paths-and-half-completed-teardown/ (Issue #695) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #695 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/695 +- Last Updated: 2026-08-29 +## Summary + +Two defects on the EFC filing (OK) chain, both carried out of the issue #637 scoping analysis and +deliberately excluded from that fix. First, the two keyboard entry points to `ActionOkAsync` have no +exception handler, so an exception thrown during filing becomes an unhandled UI-thread exception. +Second, on the button entry point the exception is caught, but `ActionOkAsync` hides the form before +awaiting the filing operation and disposes it only afterwards, so a throw leaves the form hidden, +undisposed, and uncleaned with no user-visible message. + +This entry also records a third, smaller item: `EfcDataModel.OpenOlFolderAsync` and +`OpenFsFolderAsync` assign `DestinationOlStem` verbatim in exactly the same shape that issue #637 +corrects in the `string` overload of `MoveToFolderAsync`, and are not covered by that fix. + +**Correction to the record.** Issue #637 states that an `InvalidOperationException` from +`Globals.Ol.ArchiveRootPath` "becomes an unhandled UI-thread exception" because `ButtonOK_Click` is +`async void` and rethrows. That premise is inaccurate against the current tree. +`ButtonOK_Click` is `async void`, but it delegates to `ButtonOkClickAsync`, which wraps the whole +chain in `try { ... } catch (System.Exception ex) { BoundaryErrorSink(ex.Message, ex); }` +(`QuickFiler/Controllers/EfcFormController.cs:460-475`). The exception is therefore logged, not +unhandled, on the button path. The genuine defects are the two described above, which is why they +were separated from issue #637 rather than folded into it. + +## Environment + +- OS/version: Windows 11 Pro 10.0.26200; .NET Framework 4.8.1 VSTO add-in. +- Python version: Not applicable; this is C#. +- Command/flags used: Static tracing of the EFC OK chain during issue #637 preparation research, + against `origin/main` at `ecdb1c84ba8541ab67042985919cfed4df768c01`. +- Data source or fixture: Repository source at that commit. + +## Steps to Reproduce + +1. Put the add-in into a state where `Globals.Ol.ArchiveRootPath` is unresolvable or cross-store, so + that `ArchiveRootPathGuard.RequireResolvedArchiveRoot` + (`TaskMaster/AppGlobals/ArchiveRootPathGuard.cs:32-60`) throws `InvalidOperationException`. The + value is cached only on success (`TaskMaster/AppGlobals/AppOlObjects.cs:253-267`), so the throw + recurs on every read. +2. Open the Email Filer Control form with an item selected and a valid destination chosen. +3. Path A (keyboard): trigger filing with the always-on `Keys.Return` action + (`QuickFiler/Controllers/EfcFormController.cs:392`) or the `'K'` character action routed through + `KbdExecuteAsync(ActionOkAsync)` (`:623`, `:683`; `KbdExecuteAsync` is declared at `:894-904` and + contains no try/catch). Observe an unhandled UI-thread exception. +4. Path B (button): trigger filing with the OK button. The exception is caught by + `ButtonOkClickAsync` (`:462-475`) and routed to `BoundaryErrorSink`. Observe that the form was + already hidden by `ActionOkAsync` at `:756` before the `await` at `:759`, and that + `_formViewer.Dispose(); Cleanup();` at `:769-770` never run. The item is not filed and no message + is shown. + +## Expected Behavior + +An archive-root configuration failure aborts the filing operation benignly: the user is told the +operation could not be completed, the form completes its teardown deterministically, and no +unhandled exception reaches the message pump from any entry point. Every entry point to +`ActionOkAsync` has the same exception posture. + +## Actual Behavior + +The three entry points to `ActionOkAsync` have two different exception postures. The button path +catches and logs but leaves a half-completed teardown and shows the user nothing. The two keyboard +paths do not catch at all, so the exception is unhandled on the UI thread. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: not applicable; established by source tracing. See + `QuickFiler/Controllers/EfcFormController.cs:392`, `:460-475`, `:738-772`, `:894-904`; + `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:32-47`; + `TaskMaster/AppGlobals/ArchiveRootPathGuard.cs:32-60`. + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +The trigger requires a misconfigured or cross-store archive root, so it is not expected in normal +operation. Severity is Medium because one of the two outcomes is an unhandled UI-thread exception in +a VSTO add-in, and the other silently abandons a filing operation the user believes has completed. + +## Suspected Cause / Notes + +- `EfcHomeController.ExecuteMovesAsync` (`QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:32-47`) + uses `try`/`finally` with no `catch`. The `finally` releases the `Interlocked` re-entrancy guard, + so a throw does not wedge the guard, but the exception is neither observed nor translated. +- `ActionOkAsync` (`QuickFiler/Controllers/EfcFormController.cs:738-772`) orders `Hide` at `:756` + before the `await` at `:759` and `Dispose`/`Cleanup` at `:769-770` after it. +- The narrowest seam for a benign degrade is `ExecuteMovesAsync` itself: it is the single funnel for + all three OK entry points, it already owns a `try` block, it sits below the `Hide`/`Dispose` + sequence so catching there lets `ActionOkAsync` complete its teardown, and it is already driven + headlessly by `QuickFiler.Test/Controllers/EfcHomeControllerExecuteMovesTests.cs` through the + injectable `MoveToFolderAsyncAction` seam. +- The repository already has the right notification idiom and no new shape should be invented: + `MoveFailureMessageAction` (`ExecuteMoves.cs:23-24`) and `MessageBoxShowAction` + (`EfcHomeController.cs:299-305`), both `internal Action<...>` properties defaulting to + `MessageBox.Show`. +- Any user-facing message must stay value-free. `ArchiveRootPathGuard.UnresolvableRule` and + `CrossStoreRule` (`ArchiveRootPathGuard.cs:13-17`) are already redacted and are the appropriate + text to surface. +- Separate but same defect class: `EfcDataModel.OpenOlFolderAsync` (`:299-316`, assignment at `:308`) + and `OpenFsFolderAsync` (`:318-334`, assignment at `:326`) assign `DestinationOlStem` verbatim and + read `ArchiveRootPath` the same way. Issue #637 corrects only the `string` overload of + `MoveToFolderAsync`. +- Related: issue #637 (producer-side stem normalization) and issue #614 (the archive-relative-stem + invariant and `ArchiveStemContract`). + +## Proposed Fix / Validation Ideas + +- [ ] Unit coverage areas: `ExecuteMovesAsync` catching an `InvalidOperationException` raised by the + injected `MoveToFolderAsyncAction` seam and routing it to a captured notification action; + the re-entrancy guard still released on the throwing path; the notification text carries no + archive root or mailbox identifier. +- [ ] Unit coverage areas: `ActionOkAsync` teardown ordering, so that `Dispose`/`Cleanup` run on the + throwing path as well as the succeeding one. +- [ ] Unit coverage areas: the two keyboard entry points share the button path's exception posture. +- [ ] Integration scenario to retest: an ordinary successful filing operation is unchanged, and the + existing `EfcHomeControllerExecuteMovesTests` suite still passes. +- [ ] Manual verification notes: with an unresolvable archive root, confirm each of the three OK + entry points shows the same message and leaves no undisposed form. + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch From 82ff8f2f84aa0577001fbbe9a0f1634af5c6a2bd Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sat, 29 Aug 2026 21:06:08 -0400 Subject: [PATCH 02/16] wip(637): rebase preparation artifacts onto the merged issue 638 tree Preflight round 1 found the plan materially stale after PR #700 merged. Re-anchor the plan base to the merge commit, renumber every EfcDataModel.cs citation, correct the MoveToFolder census, take the partial-class file split authorized by spec.md, and resolve the coverage-floor authority conflict. Refs #637 Co-Authored-By: Claude Sonnet 5 --- .../plan.2026-08-29T12-20.md | 904 +++++++++++++----- .../spec.md | 35 +- 2 files changed, 692 insertions(+), 247 deletions(-) diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index f8117306b..4cef22822 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -3,9 +3,9 @@ - **Issue:** #637 - **Parent (optional):** none - **Owner:** drmoisan -- **Last Updated:** 2026-08-29T12-20 +- **Last Updated:** 2026-08-29T14-05 - **Status:** Draft -- **Version:** 0.2 +- **Version:** 0.3 - **Work Mode:** full-bug (from `issue.md`); `spec.md` is the sole acceptance-criteria source (AC1-AC30). ## Conventions (read before executing any task) @@ -17,29 +17,43 @@ Every evidence path in this plan is written relative to FEATURE_DIR `FEATURE_DIR/evidence/baseline/p0-t12-csharpier-check.md`). Commands that require a literal pathspec or a literal search operand spell the folder path in full rather than using the name FEATURE_DIR, because a command carrying a placeholder cannot be executed verbatim. The sites that spell it in full -are the git pathspecs described under "Git pathspec scoping" and the token scan in P7-T10. +are the git pathspecs described under "Git pathspec scoping", the token scan in P7-T10, and the two +host-identity redaction scans in P6-T5 and P7-T11. -**Working directory** — every command below runs with the current directory set to the worktree root -`C:\Users\DanMoisan\repos\TaskMaster\.claude\worktrees\agent-a68051a23e4479267`. All repository-relative -paths in commands resolve against that root. +**Working directory** — every command below runs with the current directory set to the worktree root, +the directory containing `TaskMaster.sln` and `global.json`. All repository-relative paths in commands +resolve against that root. The worktree is chosen when the plan is executed and is deliberately not +named here: no command in this plan needs an absolute path, and writing one into a committed artifact +would embed a host account name. The placeholder convention this feature already uses for a worktree +path in prose is the one at `research/research.2026-08-29T12-30.md:6`, +`/.claude/worktrees/`. **Base commit** — the diff anchor for this plan is the literal commit -`ecdb1c84ba8541ab67042985919cfed4df768c01`. Every `git diff` in this plan supplies it explicitly. No -task pins a HEAD SHA. +`b9476588e0e49e113c73cc55cc918f4a65e022fd`. Every `git diff` in this plan supplies it explicitly. No +task pins a HEAD SHA. This anchor supersedes the earlier anchor `ecdb1c84`, which this plan was first +authored against. `origin/main` advanced to `fa2ddefa` (pull request #700, issue #638) and that work +was merged into this branch, so `ecdb1c84` is still an ancestor of `HEAD` but is no longer a clean +pre-change baseline: three files differ between it and `HEAD` for reasons this plan does not own. +`b9476588e0e49e113c73cc55cc918f4a65e022fd` is the post-merge, pre-change baseline, and P0-T8 proves +that property by a check that can fail rather than by an ancestry check that cannot. **Git pathspec scoping** — `.claude/` is a tracked directory in this repository and carries unrelated in-flight modifications, and `docs/features/parallel/` and `artifacts/` are owned by other processes. Every `git status --porcelain` and `git diff` gate in this plan is therefore scoped with an explicit pathspec naming only first-party source, test and feature-document trees. The feature-document component of every such pathspec is this feature's own folder and never the parent directory -`docs/features/active`. That narrowing is load-bearing rather than cosmetic, and its justification is -forward-looking rather than a claim about the tree as it stands today. At the time this plan was -authored no sibling folder under `docs/features/active` is untracked in this worktree: a -`git status --porcelain` span over that parent directory lists only this feature's own folder, and the -sibling folders are committed — including -`docs/features/active/2026-08-07-breadcrumb-left-right-arrow-parent-child-navigation-440`, whose paths -were confirmed present in this worktree's git index. The narrowing is required regardless, because the -executor runs later than this planning pass: this repository carries several concurrent worktrees and +`docs/features/active`. That narrowing is load-bearing rather than cosmetic, and it is required by the +tree as it stands now as well as by the tree the executor will meet. A sibling folder is untracked in +this checkout at the time of this revision: +`docs/features/active/2026-08-07-breadcrumb-left-right-arrow-parent-child-navigation-440` exists on +disk, carrying `issue.md`, `plan.2026-08-29T00-22.md` and `spec.md`, and is reported as untracked by +`git status --porcelain`. An earlier revision of this plan asserted the opposite — that no sibling +folder was untracked and that the 440 folder's paths were present in this worktree's git index — and +that assertion was wrong for this worktree; it is corrected here rather than carried forward, because +a false premise under a correct conclusion is a defect a later reader would have to rediscover. A +`git add` over the parent directory would therefore stage that other feature's folder onto this branch +today, not merely under some future condition. The narrowing is also required prospectively, because +the executor runs later than this planning pass: this repository carries several concurrent worktrees and in-flight feature folders, and a concurrent run in this checkout can leave an untracked or modified sibling folder under `docs/features/active` at any point between planning and execution. A `git add` over the parent directory would then stage and commit another feature's folder onto this branch, and a @@ -48,7 +62,7 @@ gate that consumes it unsatisfiable. This plan does not assume that the tree it time is the tree the executor will meet, so every gate is scoped to paths this plan owns. The default pathspec is therefore `-- QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`; -P6-T5 narrows it to `-- QuickFiler QuickFiler.Test` for its post-commit cleanliness check, and P8-T30 +P6-T6 narrows it to `-- QuickFiler QuickFiler.Test` for its post-commit cleanliness check, and P8-T30 widens it to the nine production and test trees that task audits. No pathspec in this plan names `.claude/`, `docs/features/parallel/`, `artifacts/`, or the bare `docs/features/active`. An unscoped gate is unsatisfiable here and must not be substituted. @@ -77,19 +91,56 @@ no other evidence artifact of this feature is excluded from it. Evidence artifac feature folders lie outside the scan's directory operand entirely, and this plan neither reads nor changes them. +**Evidence transcript redaction (host-identity discipline).** Every artifact this plan writes under +`evidence/` is committed, and several of them record captured stdout verbatim. MSBuild prints absolute +project paths, and vstest names its TRX file after the account and the machine, so a verbatim +transcript carries the host account name and the machine name into a committed file. This repository +has already had to promote host-identity leaks in committed artifacts to a tracked issue, and no hook +catches this class: `enforce-evidence-locations.ps1` checks where an artifact is written, not what it +contains. Therefore, before any evidence artifact is written, every occurrence of the absolute +worktree path inside the captured text is replaced with the literal ``, and every +remaining absolute path that begins with the Windows per-user profile root is replaced with +``. A third replacement is required and is not covered by the first two: vstest composes +its results file name from the account name and the machine name, so a transcript line naming that +file still carries host identity after its directory prefix has been redacted. Every occurrence of a +vstest results file name is therefore replaced with the placeholder ``, which deliberately +carries no extension, so that the redaction can be gated by searching for the extension itself. The rule applies to every recording task in this plan — P0-T12, P0-T13, P0-T14, +P2-T8, P2-T9, P3-T4, P4-T3, P5-T6, P6-T4, P7-T1, P7-T2, P7-T3 and P7-T4 — and to the TRX-derived +content P2-T13 quotes. Two sweep tasks verify it, P6-T5 before the Phase 6 commit and P7-T11 before +the Phase 7 commit. No third sweep is scheduled for Phase 8: every Phase 8 artifact records +repository-relative paths, search results and check-off statements, and none captures a toolchain +transcript. The per-write rule above nonetheless binds every Phase 8 artifact as well. +Each sweep task runs two searches over this feature's evidence tree and requires zero matches from +both: one for the fixed string `C:\Users`, and one for the fixed string `.trx`. That two-component +path prefix is sufficient for the first search and is deliberately the whole pattern: on Windows the +account segment always follows it immediately, so a +zero-match result over the prefix proves the account segment is absent too, and the account name is +therefore never spelled anywhere in this plan or in any artifact it produces. The second search covers +the results-file case the first cannot reach, because that file name carries the account and the +machine with no preceding profile path. Each sweep task excludes +its own artifact from its own scan by an explicit `--glob` exclusion, for the same reason P7-T10 +excludes its own artifact: the artifact must record its own `Command:` line, and that command's +pattern is the string being searched for. P7-T11 additionally excludes P6-T5's artifact, because that +earlier artifact carries the same `Command:` line and is inside P7-T11's scan scope. The restriction +to the `**/evidence/**` subtree is +load-bearing for the same reason it is in P7-T10: this plan file and `spec.md` both sit at the root of +the feature folder, and this plan file necessarily spells the search string in the two task texts that +run the gate, so a scan of the folder without the evidence-subtree glob could never return 0. + **PowerShell invocation form** — every MSBuild and vstest command is issued through `pwsh -NoProfile -Command '...'` with outer single quotes and inner double quotes. A bare `/m` passed to a POSIX shell layer is rewritten to a path and MSBuild fails with MSB1008. Every acceptance -condition expressed as a PowerShell expression — including every `(Get-Content -LiteralPath ...).Count` -check in P2-T2, P2-T4, P4-T1, P6-T1 and P6-T3, and every `Test-Path` check in P0-T9 and P0-T11 — is -likewise issued through `pwsh -NoProfile -Command '...'` with outer single quotes and inner double -quotes. Only `git` and `rg` invocations are issued directly. +condition expressed as a PowerShell expression is likewise issued through +`pwsh -NoProfile -Command '...'` with outer single quotes and inner double quotes. The +`(Get-Content -LiteralPath ...).Count` sites are P1-T7, P2-T1, P2-T2, P2-T4, P2-T5, P2-T7, P4-T1, +P5-T5, P6-T1, P6-T3 and P7-T9; the `Test-Path` sites are P0-T9, P0-T11 and P1-T7. Only `git` and `rg` +invocations are issued directly. **Search invocation form** — every `rg` invocation in this plan is issued with its pattern in single quotes. `-F` is used only where the pattern is a fixed string whose regex metacharacters — a literal backslash, or a parenthesis — must match those same characters in the target text, and in that case -a backslash is written once. This plan has exactly two such sites, P2-T3 and P5-T1, and both spell -`-F` in their own task text. Every other `rg` pattern in this plan is a regular expression whose +a backslash is written once. This plan has exactly six such sites — P2-T3, P2-T6, P5-T1, P5-T3, P6-T5 +and P7-T11 — and each spells `-F` in its own task text. Every other `rg` pattern in this plan is a regular expression whose backslashes are regex escape sequences; those patterns are issued in single quotes without `-F`, because `-F` would match the escape sequences as literal text and return zero matches for text that is present. A @@ -162,12 +213,22 @@ Findings that this plan depends on: `sources` node already exists), so it yields the same six numbers in both cases. - The enforced repository floor in the runner is **80 percent line coverage** (`Helpers.ps1:487-489`: the percentage is assigned at `:486`, the enforcing comparison `if ($percentage -lt 80)` is at - `:487`, and the `80%` message literal is at `:489`), - which matches CLAUDE.md. `.claude/rules/general-unit-test.md` states 85 percent line and 75 percent - branch. This plan reports the repository-wide figure and treats the runner's own 80 percent gate as - blocking; the change-scoped gates (no changed line loses coverage, new helper fully covered) are - blocking regardless of which repository-wide figure is quoted. The conflict is recorded, not - resolved, by this plan. + `:487`, and the `80%` message literal is at `:489`). + +**Coverage-floor authority resolution.** Two repository documents state different repository-wide +coverage figures. `CLAUDE.md` states a floor of **80 percent line coverage on the testable +denominator**, together with a maintainer-ratified COM/VSTO/WinForms exemption. +`.claude/rules/general-unit-test.md` states 85 percent line and 75 percent branch. The conflict is +resolved here by authority ordering rather than by a new exception. `policy-compliance-order` ranks +`CLAUDE.md` first and `.claude/rules/general-unit-test.md` third, so `CLAUDE.md`'s 80 percent line +floor on the testable denominator is the binding repository-wide gate for this plan. It is also the +figure the runner itself enforces, at `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487`, so +the binding gate and the executed gate are the same number. The 85 percent line and 75 percent branch +figures in `.claude/rules/general-unit-test.md` are the stricter non-binding target and are superseded +by `CLAUDE.md` where they conflict; this plan still records the post-change figure against them as a +non-blocking observation, so the gap remains visible without gating the work. The change-scoped gates +— no changed line loses coverage, and the new helper is fully covered — are blocking regardless of +which repository-wide figure is quoted. P7-T8 states this resolution and applies it. **Formatting observables.** `dotnet tool run csharpier format .` rewrites files and still exits 0, so its exit code alone proves nothing. The discriminating observation used by this plan is therefore a @@ -179,11 +240,11 @@ tree observation. `.csharpierignore` excludes `**/evidence/**`, `*.cobertura.xml `*.csproj`, `*.props` and `*.targets`, so evidence artifacts, coverage documents and the test project file are outside the formatter's scope. -**Anchored-diff form.** Before P6-T5 commits, nothing this plan changes is in `HEAD`, so a two-dot +**Anchored-diff form.** Before P6-T6 commits, nothing this plan changes is in `HEAD`, so a two-dot `BASE..HEAD` diff reports nothing for it. Every pre-commit diff gate in this plan therefore uses the -index form `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- ` and is preceded in +index form `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- ` and is preceded in the same task by a `git add` over the same paths. Every post-commit diff gate uses -`git diff ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- `. Both forms are anchored to an +`git diff b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- `. Both forms are anchored to an explicit ref; the bare unanchored `git diff` is never used. **Name-listing diffs carry a companion.** A `git diff --name-only` or `--name-status` enumerates @@ -193,7 +254,7 @@ plan creates and leaves uncommitted. Every name-listing diff in this plan theref executor must observe in that companion output. The two mechanisms are complementary and each alone is wrong in one state: the anchored diff is blind to untracked files, and porcelain status goes empty once the change is committed. This plan contains exactly two name-listing diff sites, and both carry a -companion. P6-T5 runs +companion. P6-T6 runs `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` before its diff and asserts that `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output after the commit. P8-T30 runs a porcelain span over the nine trees it audits, ahead of its two diffs, and @@ -219,21 +280,60 @@ In scope, exactly four changes: `ArchiveStemContract.TryMakeArchiveRelative` at line 99, commit the stem when non-empty, and treat an empty stem as a deterministic non-selection with a value-free diagnostic. The change stays nested inside the existing `ArchiveStemContract.IsFullOutlookPath(selection)` arm. -- **B.** `QuickFiler/Controllers/EfcDataModel.cs` — one new pure `internal static` helper called from - the `DestinationOlStem` assignment at line 287 in the `string` overload of `MoveToFolderAsync`. +- **B.** One new pure `internal static` helper, `EfcDataModel.ToFilingStemOrVerbatim`, declared in a + new partial-class file `QuickFiler/Controllers/EfcDataModel.FilingStem.cs` and called from the + `DestinationOlStem` assignment at `QuickFiler/Controllers/EfcDataModel.cs:337` in the `string` + overload of `MoveToFolderAsync`. Change B therefore also makes two supporting edits: the class + declaration at `EfcDataModel.cs:21` gains the `partial` keyword, and + `QuickFiler/QuickFiler.csproj` gains one `` item for the new file. - **C.** `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` — one assertion, one test method name, and one two-line arrange comment, recorded as a deliberate spec correction. - **D.** Three stale "deferred to issue #637" records. -Out of scope and owned by issue #695: the `Globals.Ol.ArchiveRootPath` benign degrade, the unhandled -keyboard entry points to `ActionOkAsync`, the half-completed button-path teardown, and the verbatim -`DestinationOlStem` assignments in `EfcDataModel.OpenOlFolderAsync` and `OpenFsFolderAsync`. No task in -this plan touches any of those. +**Why change B is split into a second file.** `spec.md:414-416` authorizes this: "if it does not, the +helper moves to its own file rather than the 500-line limit being exceeded." That condition is met. +`QuickFiler/Controllers/EfcDataModel.cs` is 485 lines on the merged tree, so its headroom to the +500-line limit in `.claude/rules/general-code-change.md` is 15 lines. The final helper needs roughly +24 to 26 lines: five to eight lines of XML documentation that must state the contract, describe the +second parameter, express the gate without naming `IsFullOutlookPath`, and express totality without +using the character sequence `throw`; plus a body of about twelve lines, because the call +`ArchiveStemContract.TryMakeArchiveRelative(candidatePath, archiveAncestor, out string stem)` at +eight-space indentation exceeds CSharpier's default print width of 100 columns once wrapped in an +`if (` at twelve-space indentation, so that call occupies more than one line. Even the +behavior-preserving seam alone, had it been placed in `EfcDataModel.cs`, would have taken that file to +roughly 498 lines, leaving no room for the Phase 4 body. Placing the helper in +`EfcDataModel.cs` therefore cannot satisfy the 500-line limit, and the split is taken. The helper is a +partial-class member of the same type, so the fixed identifier `EfcDataModel.ToFilingStemOrVerbatim` +that AC11 and the "Fixed identifiers" section name is preserved exactly. In-place single-line +substitutions are the only edits made to `EfcDataModel.cs` itself, so that file's line count is +unchanged by this plan and no line number in it shifts. + +Out of scope and owned by issue #695: the unhandled keyboard entry points to `ActionOkAsync`, the +half-completed button-path teardown, and the verbatim `DestinationOlStem` assignments in +`EfcDataModel.OpenOlFolderAsync` at `EfcDataModel.cs:364` and `OpenFsFolderAsync` at `:388`. No task in +this plan touches any of those. The `Globals.Ol.ArchiveRootPath` benign-degrade item is no longer +pending in `EfcDataModel`: issue #638 delivered it there, adding `TryGetArchiveRoot` at +`EfcDataModel.cs:271-297` with the guarded read at `:284` and the +`UserDiagnosticAction(ArchiveRootUnavailableMessage)` degrade at `:358` and `:382`. This plan +preserves that work unchanged and adds nothing to it; `spec.md:164-172` still describes the whole +benign-degrade item as pending, and that staleness is recorded by P8-T32 rather than corrected in +`spec.md`. ## Fixed identifiers (the executor does not choose these) - New helper: `EfcDataModel.ToFilingStemOrVerbatim(string candidatePath, string archiveAncestor)`, `internal static string`. +- New production file: `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, namespace + `QuickFiler.Controllers`, whose only type declaration is written verbatim as + ` internal partial class EfcDataModel` and whose only member is `ToFilingStemOrVerbatim` with its + XML documentation. Its `using` set is exactly `using UtilitiesCS.OutlookObjects.Folder;`, which is + the namespace of `ArchiveStemContract` (`UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:4`, + type at `:18`) and is the only namespace the helper body needs. +- Amended declaration line in `QuickFiler/Controllers/EfcDataModel.cs:21`: ` internal class EfcDataModel` + becomes ` internal partial class EfcDataModel`. This is a one-token, single-line substitution; the + file's line count and every line number in it are unchanged by it. +- New project item in `QuickFiler/QuickFiler.csproj`, written verbatim as + ` `. - New test file: `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`, class `BreadcrumbBridgeRouterIssue637Tests`, with exactly these ten test methods: `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected`, @@ -278,9 +378,11 @@ this plan touches any of those. These were re-derived against the working tree and one disagrees with `spec.md`. They are recorded so no downstream artifact inherits a wrong figure. -1. `QuickFiler/Controllers/EfcDataModel.cs` is **423** lines, not the 424 stated in the spec's - implementation table and in AC25's parenthetical. Headroom to the 500-line limit is 77, not 76. - AC25's binding clause ("at or under 500 lines") is unaffected. +1. `QuickFiler/Controllers/EfcDataModel.cs` is **485** lines on the merged tree — 423 at the + `ecdb1c84` planning base, and 424 in the spec's implementation table and in AC25's parenthetical. + Headroom to the 500-line limit is **15**. This is what forces the change-B file split described + under "Scope". AC25's binding clause ("at or under 500 lines") is unaffected, and AC25's + parenthetical figure is corrected to 485 in `spec.md`. 2. The composition test `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` spans `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:167-214`; AC23 and the spec cite `:167-213`. The closing brace is at 214. No behavioral consequence. @@ -288,10 +390,32 @@ no downstream artifact inherits a wrong figure. the spec cites `:143-146`. The write at `:145` and the read at `:143` that AC24 names are exact. 4. Research section 11's claim that no `EfcDataModelTests.cs` exists is wrong; the file exists at 409 lines. `spec.md` already records this correction and the spec wins. -5. Research section 6's "16 matching lines across 6 files" for the `MoveToFolder` family is 16 lines - across **5** files on the tree. `spec.md` already records this correction and the spec wins. +5. The `MoveToFolder` family census moved when issue #638 merged. On the merged tree the family-stem + search returns **23** lines across **6** files, and the syntax-anchored search returns **10** lines + across **5** files: 3 declarations and 7 call sites, leaving 13 residual non-member textual + references. Research section 6 says 16 lines across 6 files and `spec.md` says 16 across 5; both + describe the pre-merge tree. AC16's counts and citations are corrected in `spec.md`; P1-T2 and + P8-T16 carry the measured figures. The sixth file in the stem search is + `QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs`, added by issue #638. 6. This worktree has no `.dotnet-sdk` directory and no `packages` directory, so the repo-local SDK and the NuGet package restore must both be bootstrapped before any toolchain command runs. +7. Issue #638 landed on this branch before execution begins. It changed + `QuickFiler/Controllers/EfcDataModel.cs` (+68/-3), added one `` line at + `QuickFiler.Test/QuickFiler.Test.csproj:116`, and added the 389-line test file + `QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs` with 11 `[TestMethod]` members and a + `private sealed class TestableEfcDataModel : EfcDataModel` at `:377`. That subclass derives from + `EfcDataModel`, and adding `partial` to a class declaration does not change its accessibility, its + base list, or its members, so the Phase 2 `partial` edit leaves it compiling unchanged. This plan + interacts with that file in exactly two ways: it must not break + `MoveToFolderAsync_WhenArchiveRootResolves_StillReadsItOnce` at `:172`, whose + `olObjects.VerifyGet(value => value.ArchiveRootPath, Times.Once());` at `:185` pins a single + archive-root read per call; and the file contributes one call site at `:314` to the `MoveToFolder` + census. It contains no `DestinationOlStem`, no `ToFilingStem` and no `SelectedFolderPath`, so + changes A, C and D do not touch it. +8. `QuickFiler/QuickFiler.csproj` is a non-SDK-style project with **130** explicit `` + items. The `ItemGroup` carrying the `Controllers\` entries opens at `:287`, and + `` is at `:289`. A production file absent from + this project does not compile into the assembly. ### Phase 0 — Context, policy reads, and baseline capture @@ -322,14 +446,21 @@ no downstream artifact inherits a wrong figure. Acceptance: the artifact names the `EfcDataModelTests.cs` existence correction, the `MoveToFolder` five-file correction, and the `SelectedFolderPath` three-production-file correction. -- [ ] [P0-T8] Record the branch and base commit. Run +- [ ] [P0-T8] Record the branch and prove the base commit is a clean pre-change baseline. Run `git rev-parse --abbrev-ref HEAD`, `git rev-parse HEAD`, and - `git merge-base --is-ancestor ecdb1c84ba8541ab67042985919cfed4df768c01 HEAD`, and write - `evidence/baseline/p0-t8-git-base.md`. Acceptance: the `merge-base --is-ancestor` invocation - exits 0, and the recorded branch name is - `bug/breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. If the branch name - differs, record `BRANCH MISMATCH` in the artifact, stop, and report to the orchestrator; do not - proceed to P0-T9. + `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler QuickFiler.Test`, + and write `evidence/baseline/p0-t8-git-base.md` recording all three outputs verbatim. + Acceptance: the `git diff --name-only` invocation produces **no output at all**, and the + recorded branch name is + `bug/breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. An ancestry check is + deliberately not used here. `git merge-base --is-ancestor` exits 0 for any ancestor, including + an ancestor that predates work this plan does not own, so it cannot fail in the state this task + exists to detect; the empty-diff form fails as soon as any file under `QuickFiler` or + `QuickFiler.Test` differs between the anchor and `HEAD`, which is the property every later + "exactly N paths", "no hunk in range" and "added line numbers" gate depends on. If the branch + name differs, or if the diff produces any output, record `BASE MISMATCH` in the artifact + together with the offending output, stop, and report to the orchestrator; do not proceed to + P0-T9. - [ ] [P0-T9] Bootstrap the repo-local .NET SDK with `pwsh -NoProfile -File scripts/vscode/Install-RepoDotNetSdk.ps1` and write `evidence/baseline/p0-t9-sdk-bootstrap.md`. Acceptance: `EXIT_CODE: 0`, and after the run the @@ -402,9 +533,12 @@ no downstream artifact inherits a wrong figure. failing test's fully qualified name (empty when the run passes). The file `coverage/p0-t15-baseline.cobertura.xml` exists after the run. `Output Summary:` additionally carries the six numeric `/coverage` attribute values and the derived line and branch - percentages that P0-T16 reads, copied in once P0-T16 has produced them; this task is not - complete until that copy-back has been made, because the plan contract requires the baseline - test-step artifact itself to carry the numeric coverage headline. + percentages that P0-T16 reads, copied in once P0-T16 has produced them, because the plan + contract requires the baseline test-step artifact itself to carry the numeric coverage + headline. The execution order for the pair is explicit and is not a deadlock: run this task's + command and write everything except the copied-back headline, leave P0-T15 **unchecked**, + execute P0-T16 in full, then return to P0-T15, write the copy-back into `Output Summary:`, and + check both tasks off together. - [ ] [P0-T16] Read the baseline numeric coverage headline. Run `pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; $c = $d.SelectSingleNode("/coverage"); foreach ($a in @("line-rate","branch-rate","lines-covered","lines-valid","branches-covered","branches-valid")) { $a + "=" + $c.GetAttribute($a) }'` and write `evidence/baseline/p0-t16-coverage-headline.md`. Acceptance: `EXIT_CODE: 0`, and @@ -417,7 +551,12 @@ no downstream artifact inherits a wrong figure. `pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; foreach ($f in @("QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcDataModel.cs")) { $u = @(); foreach ($c in $d.SelectNodes("//class")) { if ($c.GetAttribute("filename") -eq $f) { foreach ($l in $c.SelectNodes("./lines/line")) { if ([int]$l.GetAttribute("hits") -eq 0) { $u += [int]$l.GetAttribute("number") } } } }; $f + " uncovered=" + (($u | Sort-Object -Unique) -join ",") } '` and write `evidence/baseline/p0-t17-baseline-uncovered-lines.md`. Acceptance: `EXIT_CODE: 0`, and the artifact records one `uncovered=` line for each of the two file paths, even when the set is - empty. + empty. The third production file this plan touches, + `QuickFiler\Controllers\EfcDataModel.FilingStem.cs`, is deliberately absent from this command: + it does not exist on the baseline tree, so it has no baseline coverage row and no baseline + uncovered set. The artifact records that absence in a line reading + `QuickFiler\Controllers\EfcDataModel.FilingStem.cs baseline=absent`, which is the baseline + P7-T7 compares its post-change measurement of that file against. ### Phase 1 — Pre-change census re-derivation, two independent searches per number @@ -439,20 +578,23 @@ second, independently constructed search. No number in this phase is verified by (`BreadcrumbBridgeRouter.Selection.cs:83` and `:109`) and call sites total 7 (`BreadcrumbBridgeRouter.cs:201`, `:286`, `BreadcrumbBridgeRouter.Arrows.cs:138`, `:153`, `:161`, `BreadcrumbBridgeRouter.Selection.cs:33`, `:47`). -- [ ] [P1-T2] Re-derive the `MoveToFolder` family census (AC16: 3 declarations, 6 call sites). +- [ ] [P1-T2] Re-derive the `MoveToFolder` family census (AC16: 3 declarations, 7 call sites). Search 1, family-stem: `rg -n "MoveToFolder" --glob "*.cs" .` — the bare stem catches any non-`Async` sibling or partially renamed overload that an `Async`-suffixed pattern would miss. Search 2, independently constructed on invocation and declaration syntax: `rg -n "MoveToFolderAsync\s*\(" --glob "*.cs" .` — this excludes the `MoveToFolderAsyncAction` delegate property, its null test and its invocation, which are textual references rather than family members. Write `evidence/baseline/p1-t2-movetofolder-family.md`. Acceptance: Search 1 - returns 16 lines across 5 files; Search 2 returns 9 lines across 4 files; the artifact classifies - Search 2's 9 lines as exactly 3 declarations (`EfcDataModel.cs:259`, `EfcDataModel.cs:336`, - `EfcHomeController.ExecuteMoves.cs:89`) and 6 call sites - (`EfcHomeController.ExecuteMoves.cs:78`, `:98`, `EfcDataModel.cs:346`, `EfcFormController.cs:537`, - `:844`, `EfcHomeControllerExecuteMovesTests.cs:87`); and the artifact records that Search 1 minus - Search 2 leaves exactly 7 non-member textual references, closing the 16-line accounting. The - artifact also records that the file count is 5, not the 6 stated in research section 6. + returns 23 lines across 6 files; Search 2 returns 10 lines across 5 files; the artifact classifies + Search 2's 10 lines as exactly 3 declarations (`EfcDataModel.cs:303`, `EfcDataModel.cs:398`, + `EfcHomeController.ExecuteMoves.cs:89`) and 7 call sites + (`EfcHomeController.ExecuteMoves.cs:78`, `:98`, `EfcDataModel.cs:408`, `EfcFormController.cs:537`, + `:844`, `EfcHomeControllerExecuteMovesTests.cs:87`, + `EfcDataModelArchiveRootTests.cs:314`); and the artifact records that Search 1 minus + Search 2 leaves exactly 13 non-member textual references, closing the 23-line accounting. The + artifact also records that the stem-search file count is 6 and the syntax-anchored file count is + 5, that the sixth stem-search file is `EfcDataModelArchiveRootTests.cs`, and that the 16-line + figure in research section 6 and in `spec.md` describes the tree before issue #638 merged. - [ ] [P1-T3] Re-derive the `SelectedFolderPath` surface (AC24: 9 lines across 3 production files, 2 writes, 3 reads). Search 1: `rg -c "SelectedFolderPath" --glob "*.cs" .`, recording the per-file counts. Search 2, independently constructed by scoping to the production project directories up @@ -503,12 +645,19 @@ second, independently constructed search. No number in this phase is verified by Construction 2, independently constructed with a line-oriented search rather than a file read: `rg -c "^" --glob "*.cs" QuickFiler/Controllers/ QuickFiler.Test/Controllers/` filtered to the same six paths. `Measure-Object -Line` must not be substituted for `(Get-Content).Count`; it - reports a different figure for a file without a trailing newline. Write - `evidence/baseline/p1-t7-file-line-counts.md`. Acceptance: both constructions agree on all six - paths; `EfcDataModel.cs` is 423; `BreadcrumbBridgeRouter.Selection.cs` is 209; + reports a different figure for a file without a trailing newline. In the same task run + `pwsh -NoProfile -Command 'Test-Path "QuickFiler\Controllers\EfcDataModel.FilingStem.cs"'` to + establish the seventh path's baseline. That path is checked with `Test-Path` rather than added + to the two line-count constructions because this plan creates it in Phase 2 and + `Get-Content -LiteralPath` on an absent path throws, which would make this task unsatisfiable. + Write `evidence/baseline/p1-t7-file-line-counts.md`. Acceptance: both constructions agree on all + six paths; `EfcDataModel.cs` is 485; `BreadcrumbBridgeRouter.Selection.cs` is 209; `EfcSelectionGuard.cs` is 79; `BreadcrumbBridgeRouterIssue439Tests.cs` is 694; - `EfcDataModelIssue614Tests.cs` is 123; `EfcSelectionGuardTests.cs` is 296; and the artifact - records that `spec.md` states 424 for `EfcDataModel.cs` and that the tree value 423 governs. + `EfcDataModelIssue614Tests.cs` is 123; `EfcSelectionGuardTests.cs` is 296; the `Test-Path` result + for `QuickFiler\Controllers\EfcDataModel.FilingStem.cs` is `False` and is recorded as the + baseline for that path; and the artifact records that `spec.md`'s implementation table and AC25 + state 424 for `EfcDataModel.cs`, that the `ecdb1c84` planning base had 423, and that the merged + tree value 485 governs, leaving 15 lines of headroom to the 500-line limit. - [ ] [P1-T8] Re-derive the single pinning assertion (AC20: exactly 1 existing assertion changes). Construction 1, on the assertion form: `rg -n "SelectedFolderPath\.Should\(\)\.Be\(" --glob "*.cs" QuickFiler.Test/` with every hit @@ -526,37 +675,103 @@ second, independently constructed search. No number in this phase is verified by The change-B tests name a member that does not exist yet, and a test file referencing a missing member makes the whole `QuickFiler.Test` assembly fail to compile, which would prevent every other test in -this phase from running at all. P2-T1 therefore lands a behavior-preserving seam first: the helper is -declared and called, but returns its input verbatim, which is byte-for-byte the behavior of the current -assignment at `EfcDataModel.cs:287`. The red in this phase is a genuine runtime red, not a compile -failure. +this phase from running at all. P2-T1 through P2-T4 therefore land a behavior-preserving seam first: +the helper is declared and called, but returns its input verbatim, which is byte-for-byte the behavior +of the current assignment at `EfcDataModel.cs:337`. The red in this phase is a genuine runtime red, not +a compile failure. + +The seam is delivered as a partial-class member in a new file, for the 500-line reason stated under +"Scope". Three tasks are therefore prerequisites of the seam and are ordered ahead of it here rather +than left to Phase 4: the `partial` keyword on the existing declaration (P2-T1), the new file itself +(P2-T2), and the project registration that makes the new file compile (P2-T3). Only then does P2-T4 +redirect the assignment. Reversing any of those orders produces a build that does not compile. -- [ ] [P2-T1] Add the behavior-preserving seam. In `QuickFiler/Controllers/EfcDataModel.cs`, declare - `internal static string ToFilingStemOrVerbatim(string candidatePath, string archiveAncestor)` +- [ ] [P2-T1] Make the existing declaration partial. In `QuickFiler/Controllers/EfcDataModel.cs`, + replace line 21, ` internal class EfcDataModel`, with + ` internal partial class EfcDataModel`. This is a one-token, single-line substitution: the + file stays at 485 lines and no line number in it shifts, which is what lets every line citation + in Phase 4 and Phase 8 remain valid after the edit. Adding `partial` changes no accessibility, no + base list and no member, so + `QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs:377`, which declares + `private sealed class TestableEfcDataModel : EfcDataModel`, continues to compile unchanged. + Acceptance: + `rg -n "internal partial class EfcDataModel" QuickFiler/Controllers/EfcDataModel.cs` returns + exactly 1 line and it is line 21; + `rg -n "^ internal class EfcDataModel$" QuickFiler/Controllers/EfcDataModel.cs` returns 0 + lines; and + `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.cs").Count'` + reports exactly 485. +- [ ] [P2-T2] Create the new partial-class file + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`. It contains exactly one `using` directive, + `using UtilitiesCS.OutlookObjects.Folder;`, the namespace `QuickFiler.Controllers`, the type + declaration written verbatim as ` internal partial class EfcDataModel`, and one member: the + seam form of `internal static string ToFilingStemOrVerbatim(string candidatePath, string archiveAncestor)` whose body is exactly `_ = ArchiveStemContract.TryMakeArchiveRelative(candidatePath, archiveAncestor, out _);` followed - by `return candidatePath;`, with an XML documentation comment stating that this is the #637 seam - and that the normalization lands in P4-T1. Change the assignment at `EfcDataModel.cs:287` from - `DestinationOlStem = folderpath,` to - `DestinationOlStem = ToFilingStemOrVerbatim(folderpath, Globals.Ol.ArchiveRootPath),`. - The explicit discard on the `TryMakeArchiveRelative` call is required so that both parameters are - used and no unused-parameter diagnostic can be promoted to an error by - `/p:TreatWarningsAsErrors=true`. Declare the helper immediately after the closing brace of the - `string` overload of `MoveToFolderAsync` at original line 297 and before - `internal async Task OpenOlFolderAsync` at original line 299, so the insertion hunk falls - outside both ranges P4-T6 excludes. Do not place it adjacent to `ToArchiveRelativeStem`. The + by `return candidatePath;`, carrying an XML documentation comment stating that this is the #637 + seam and that the normalization lands in P4-T1. The explicit discard on the + `TryMakeArchiveRelative` call is required so that both parameters are used and no + unused-parameter diagnostic can be promoted to an error by `/p:TreatWarningsAsErrors=true`. The seam's XML documentation must not contain the token `MoveToFolder`; refer to its caller as "the `string` filing overload" instead, because P8-T16 asserts the family stem search still - returns exactly 16 lines. For the same reason the seam's XML documentation must not contain - either of the two literals this task asserts an exact count of 1 for — + returns exactly 23 lines. For the same reason the seam's XML documentation must not contain + either of the two literals whose exact count of 1 is asserted below — `internal static string ToFilingStemOrVerbatim` and `DestinationOlStem = ToFilingStemOrVerbatim` — so it must not reproduce the declaration signature or the assignment statement; naming the - method by its bare identifier is permitted and is classified rather than counted by P4-T2. - Acceptance: `rg -n "internal static string ToFilingStemOrVerbatim" QuickFiler/Controllers/EfcDataModel.cs` - returns exactly 1 line, and + method by its bare identifier is permitted and is classified rather than counted by P4-T2. Three + further tokens are barred from this file, in its documentation and in its body alike, because + P4-T1's acceptance asserts each of their pre-edit counts in this file: `IsFullOutlookPath`, which + P4-T1 requires to be 0 before its edit and 1 after; the character sequence `throw` in any form, + including `throws`, which P4-T1 requires to be absent both before and after; and `Globals` in any + form, which P4-T1 requires to be absent. The seam body given above uses none of the three. The + two literals this task creates are quoted verbatim here because both are absent from the tracked + tree until this task writes them: + `internal static string ToFilingStemOrVerbatim` and `internal partial class EfcDataModel`. + Acceptance: the file exists; + `rg -n "internal static string ToFilingStemOrVerbatim" QuickFiler/Controllers/EfcDataModel.FilingStem.cs` + returns exactly 1 line; + `rg -n "internal partial class EfcDataModel" QuickFiler/Controllers/EfcDataModel.FilingStem.cs` + returns exactly 1 line; and + `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.FilingStem.cs").Count'` + is at most 500. +- [ ] [P2-T3] Register the new production file in the non-SDK project. Insert + ` ` into + `QuickFiler/QuickFiler.csproj` immediately after the existing line 289, + ` `, which sits inside the `ItemGroup` that + opens at line 287. That project carries 130 explicit `` items and no wildcard + glob, so a production file absent from it compiles into nothing and the seam would not exist at + run time. The literal this task creates is `Controllers\EfcDataModel.FilingStem.cs`, quoted here + verbatim because it is absent from the tracked tree until this task inserts it. The acceptance + search below is the fixed-string, single-quoted form required by the "Search invocation form" + convention: the backslash is written once and `-F` disables regex interpretation, so no shell + layer and no regex engine can consume it. Acceptance: + `rg -F -n 'Controllers\EfcDataModel.FilingStem.cs' QuickFiler/QuickFiler.csproj` + returns exactly 1 line; that line is line 290; and it is inside the same `ItemGroup` that begins + at line 287. +- [ ] [P2-T4] Redirect the `DestinationOlStem` assignment to the seam. In + `QuickFiler/Controllers/EfcDataModel.cs`, change line 337 from + ` DestinationOlStem = folderpath,` to + ` DestinationOlStem = ToFilingStemOrVerbatim(folderpath, olAncestor),`. + The second argument is the local produced by `if (!TryGetArchiveRoot(out var olAncestor))` at + line 327, and it is the same local the initializer already assigns to `OlAncestor` at line 339. + Naming `Globals.Ol.ArchiveRootPath` at line 337 is **prohibited**. Issue #638 removed exactly + that unguarded read from exactly this method and left the property read once, inside the + `try` of `TryGetArchiveRoot` at line 284. Writing the property here would reintroduce the + unguarded read and would read it twice in one call, failing the merged regression test + `MoveToFolderAsync_WhenArchiveRootResolves_StillReadsItOnce` at + `QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs:172`, whose + `olObjects.VerifyGet(value => value.ArchiveRootPath, Times.Once());` at `:185` pins the single + read. P2-T10 gates that constraint at a scoped run rather than leaving it documented, because + every other scoped run in Phases 2 through 6 is filtered to a class that would not observe it. + This is a single-line substitution, so `EfcDataModel.cs` stays at 485 lines and no line number + in it shifts. Acceptance: `rg -n "DestinationOlStem = ToFilingStemOrVerbatim" QuickFiler/Controllers/EfcDataModel.cs` - returns exactly 1 line. -- [ ] [P2-T2] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` containing + returns exactly 1 line and it is line 337; + `rg -n "Globals.Ol.ArchiveRootPath" QuickFiler/Controllers/EfcDataModel.cs` returns exactly 1 + line and it is line 284; and + `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.cs").Count'` + reports exactly 485. +- [ ] [P2-T5] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` containing the class `BreadcrumbBridgeRouterIssue637Tests` with exactly the ten test methods named in the "Fixed identifiers" section. Use fixture Shape 2 and do not invent a new fixture shape: a `[TestInitialize]` `Setup` and `[TestCleanup]` `Cleanup` modelled on @@ -571,13 +786,13 @@ failure. `BindRowsAsync` trims to empty at `BreadcrumbBridgeRouter.cs:107-109`. Framework is MSTest with Moq and FluentAssertions; no temporary file, no wall-clock wait, no Outlook process. No text in this file may contain the token `MoveToFolder`, because P8-T16 asserts that a repository-wide - `*.cs` search for that stem still returns exactly 16 lines. Acceptance: + `*.cs` search for that stem still returns exactly 23 lines. Acceptance: the file exists; `rg -c "\[TestMethod\]" QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` returns 10; each of the ten fixed method names is found exactly once by `rg -n "public void " QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`; and `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs").Count` is at most 500. -- [ ] [P2-T3] Register the new test file in the non-SDK project. Insert +- [ ] [P2-T6] Register the new test file in the non-SDK project. Insert ` ` into `QuickFiler.Test/QuickFiler.Test.csproj` immediately after the existing line 64, ` `. A file absent @@ -589,7 +804,7 @@ failure. regex engine can consume it. Acceptance: `rg -F -n 'Controllers\BreadcrumbBridgeRouterIssue637Tests.cs' QuickFiler.Test/QuickFiler.Test.csproj` returns exactly 1 line, and that line is inside the same `ItemGroup` that begins at line 57. -- [ ] [P2-T4] Add the change-B helper tests. In the existing file +- [ ] [P2-T7] Add the change-B helper tests. In the existing file `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, add a new sibling `[TestClass]` `EfcDataModelIssue637Tests` containing exactly the eight test methods named in the "Fixed identifiers" section, reaching `EfcDataModel.ToFilingStemOrVerbatim` through the existing @@ -602,27 +817,48 @@ failure. therefore asserts over the literal `class EfcDataModelIssue637Tests`, which is quoted here verbatim because it is absent from the tracked tree until this task creates it. No text this task adds may contain the token `MoveToFolder`, because P8-T16 asserts that a repository-wide - `*.cs` search for that stem still returns exactly 16 lines. Acceptance: + `*.cs` search for that stem still returns exactly 23 lines. Acceptance: `rg -n "class EfcDataModelIssue637Tests" QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` returns exactly 1 line; each of the eight fixed method names is found exactly once in that file; `git add QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` followed in the same task by - `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` + `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` shows zero removed content lines, meaning zero lines beginning with a single `-`; and `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs").Count` is at most 500. -- [ ] [P2-T5] Run the analyzer build and write `evidence/regression-testing/p2-t5-msbuild-analyzers.md` +- [ ] [P2-T8] Run the analyzer build and write `evidence/regression-testing/p2-t8-msbuild-analyzers.md` using the P0-T13 command verbatim. Acceptance: `EXIT_CODE: 0`; the output contains - `(Rebuild target(s))`; and the `Error(s)` count is 0. A non-zero exit here means the seam or the - new test files do not compile and must be repaired before P2-T7 runs. -- [ ] [P2-T6] Run the nullable build and write `evidence/regression-testing/p2-t6-msbuild-nullable.md` + `(Rebuild target(s))`; and the `Error(s)` count is 0. A non-zero exit here means the seam, the + partial-class split, the project registrations, or the new test files do not compile and must be + repaired before P2-T10 runs. +- [ ] [P2-T9] Run the nullable build and write `evidence/regression-testing/p2-t9-msbuild-nullable.md` using the P0-T14 command verbatim. Acceptance: `EXIT_CODE: 0`; the output contains `(Rebuild target(s))`; the recorded `Command:` line does not contain the solution-wide nullable opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. This gate is where an unused-parameter or nullable diagnostic introduced by the seam would surface as an error. -- [ ] [P2-T7] [expect-fail] Run the new router regression tests before the fix. Run - `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; $asm = Join-Path (Get-Location).Path "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll"; & $vstest $asm /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue637Tests&TestCategory!=LiveOutlook" /Logger:trx "/ResultsDirectory:coverage\testresults\p2-t7"; "EXIT_CODE=$LASTEXITCODE"'` - and write `evidence/regression-testing/p2-t7-router-tests-red.md` with `ExpectedExitCode: 1`. +- [ ] [P2-T10] Prove the seam did not regress issue #638's single-read guarantee. Use the scoped + vstest command stated in full in P2-T11 below, with the filter + `"/TestCaseFilter:FullyQualifiedName~EfcDataModelArchiveRootTests&TestCategory!=LiveOutlook"` + and the results directory `coverage\testresults\p2-t10`, and write + `evidence/regression-testing/p2-t10-issue638-preserved.md`. This task exists because P2-T4 + rewrites the one line issue #638 changed in the `string` overload, and every other scoped run in + Phases 2 through 6 is filtered to a class that cannot observe the result; without this task the + regression would surface for the first time at the full-suite run in P7-T5. Acceptance: the + output does not contain `No test matches the given testcase filter`; the run reports 11 tests for + that class, which is the number of `[TestMethod]` members the file carries; the artifact names + `MoveToFolderAsync_WhenArchiveRootResolves_StillReadsItOnce` individually as **passing**, + together with the statement that its `Times.Once()` assertion is what proves P2-T4 passed the + existing `olAncestor` local rather than reading `Globals.Ol.ArchiveRootPath` a second time; and + the failing set is a subset of the `BASELINE_FAILURE_SET` recorded in + `evidence/baseline/p0-t15-mstest-coverage.md`, with every still-failing baseline member named. If + that baseline set is empty, `EXIT_CODE: 0` with 11 passed and 0 failed is required. If + `MoveToFolderAsync_WhenArchiveRootResolves_StillReadsItOnce` is itself in the baseline failing + set, record `BASELINE PROTECTS NOTHING`, stop, and report to the orchestrator: the invariant this + task exists to protect would already be red before this plan ran, and no result here would + distinguish a regression from that pre-existing state. +- [ ] [P2-T11] [expect-fail] Run the new router regression tests before the fix. Run + `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; $asm = Join-Path (Get-Location).Path "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll"; & $vstest $asm /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue637Tests&TestCategory!=LiveOutlook" /Logger:trx "/ResultsDirectory:coverage\testresults\p2-t11"; "EXIT_CODE=$LASTEXITCODE"'` + and write `evidence/regression-testing/p2-t11-router-tests-red.md` with `ExpectedExitCode: 1`. Acceptance: the output does not contain `No test matches the given testcase filter`; the run reports 10 tests total; exactly these 5 fail, named individually in the artifact: `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected`, @@ -636,19 +872,24 @@ failure. `RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected`, `RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim`. A different partition is a defect in the tests, not evidence of the bug, and must be repaired before Phase 3. -- [ ] [P2-T8] [expect-fail] Run the new helper tests before the fix. Run the P2-T7 command with the +- [ ] [P2-T12] [expect-fail] Run the new helper tests before the fix. Run the P2-T11 command with the filter substring changed to `FullyQualifiedName~EfcDataModelIssue637Tests` and the results - directory changed to `coverage\testresults\p2-t8`, and write - `evidence/regression-testing/p2-t8-helper-tests-red.md` with `ExpectedExitCode: 1`. Acceptance: + directory changed to `coverage\testresults\p2-t12`, and write + `evidence/regression-testing/p2-t12-helper-tests-red.md` with `ExpectedExitCode: 1`. Acceptance: the output does not contain `No test matches the given testcase filter`; the run reports 8 tests total; exactly these 2 fail: `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` and `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem`; and the other 6 pass, because the seam already returns the input verbatim for every non-normalizable case. -- [ ] [P2-T9] Prove the new test file actually executes rather than silently compiling into nothing. - From the TRX produced by P2-T7 at `coverage\testresults\p2-t7`, extract every `UnitTestResult` +- [ ] [P2-T13] Prove the new test file actually executes rather than silently compiling into nothing. + From the TRX produced by P2-T11 at `coverage\testresults\p2-t11`, extract every `UnitTestResult` whose `testName` begins with one of the ten fixed method names, and write - `evidence/regression-testing/p2-t9-compile-include-observed.md`. Acceptance: the artifact records - exactly 10 such results; it quotes the `Compile Include` line added by P2-T3 verbatim; and it + `evidence/regression-testing/p2-t13-compile-include-observed.md`. Quote only the `testName` and + `outcome` attribute values of each result. Do not quote the results file's name, and do not quote + any absolute path: vstest composes that file name from the account and the machine, so quoting it + would put host identity into a committed artifact, and the "Evidence transcript redaction" + convention requires it to be written as `` if it must be referred to at all. + Acceptance: the artifact records + exactly 10 such results; it quotes the `Compile Include` line added by P2-T6 verbatim; and it records that removing that line would make this count 0, which is the observable AC26 requires. ### Phase 3 — Change A, producer normalization in `SelectRow` @@ -686,7 +927,7 @@ failure. pass-through mode is untouched. - [ ] [P3-T3] Verify AC8: `SelectHierarchyPath` and `CommitSelection` are unmodified. Run `git add QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` then - `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` + `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` and write `evidence/regression-testing/p3-t3-selectionfile-diff.md`. Acceptance: every hunk header in the diff addresses a line range that lies entirely within the original lines 83 to 107; no hunk touches the original line range 109 to 139; and the artifact lists the hunk headers @@ -696,12 +937,12 @@ failure. both record `EXIT_CODE: 0`; both outputs contain `(Rebuild target(s))`; and neither recorded `Command:` line contains the solution-wide nullable opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. -- [ ] [P3-T5] Run the router regression suite green. Use the P2-T7 command with the results directory +- [ ] [P3-T5] Run the router regression suite green. Use the P2-T11 command with the results directory changed to `coverage\testresults\p3-t5`, and write `evidence/regression-testing/p3-t5-router-tests-green.md`. Acceptance: `EXIT_CODE: 0`; 10 tests - total; 10 passed; 0 failed; 0 skipped; and the five tests that failed in P2-T7 are named + total; 10 passed; 0 failed; 0 skipped; and the five tests that failed in P2-T11 are named individually in the artifact as now passing. -- [ ] [P3-T6] Run the unmodified router test classes to prove no collateral regression. Use the P2-T7 +- [ ] [P3-T6] Run the unmodified router test classes to prove no collateral regression. Use the P2-T11 command with the filter `"/TestCaseFilter:(FullyQualifiedName~BreadcrumbBridgeRouterIssue614Tests|FullyQualifiedName~BreadcrumbBridgeRouterTests|FullyQualifiedName~BreadcrumbBridgeRouterQueueTests)&TestCategory!=LiveOutlook"` and the results directory `coverage\testresults\p3-t6`, and write @@ -713,8 +954,8 @@ failure. ### Phase 4 — Change B, normalization in the `string` overload of `MoveToFolderAsync` -- [ ] [P4-T1] Replace the seam body in `QuickFiler/Controllers/EfcDataModel.cs` with the real - normalization. `ToFilingStemOrVerbatim` returns `candidatePath` unchanged when +- [ ] [P4-T1] Replace the seam body in `QuickFiler/Controllers/EfcDataModel.FilingStem.cs` with the + real normalization. `ToFilingStemOrVerbatim` returns `candidatePath` unchanged when `ArchiveStemContract.IsFullOutlookPath(candidatePath)` is false; otherwise it calls `ArchiveStemContract.TryMakeArchiveRelative(candidatePath, archiveAncestor, out string stem)` and returns `stem` when that call succeeds and `stem.Length != 0`, and returns `candidatePath` @@ -722,36 +963,43 @@ failure. and touches no static mutable state. It deliberately does not adopt `ToArchiveRelativeStem`'s throw on the archive-root-exact input; the rationale is recorded in `spec.md` under "Error handling and logging updates". Update the XML documentation to state the - final contract and remove the seam wording added by P2-T1. The helper's XML documentation must + final contract and remove the seam wording added by P2-T2. The helper's XML documentation must not contain the token `MoveToFolder`; refer to its caller as "the `string` filing overload" - instead, because P8-T16 asserts the family stem search still returns exactly 16 lines. Three - further tokens are barred from that documentation for the same reason — each is a token some + instead, because P8-T16 asserts the family stem search still returns exactly 23 lines. Two + further tokens are barred from that documentation for the same reason — each is a token an acceptance condition asserts an exact count for over this same file, and the natural wording of the contract would otherwise add an occurrence. First, `IsFullOutlookPath`: AC12 phrases the contract as "The helper is gated on `ArchiveStemContract.IsFullOutlookPath`", but this task asserts an exact count of 1 for that token in this file, so the documentation states the gate as "returns its input unchanged unless the input is a full Outlook path" without naming the predicate. Second, the character sequence `throw` in any form, including `throws`: this task - compares the matched line texts of `rg -n "throw"` taken before the edit against those taken - after it and requires the two sets identical, so the totality claim is worded as - "returns a value for every input and - propagates no exception". Third, `Globals.Ol.ArchiveRootPath`: P4-T6 asserts an exact count of 4 - for that token in this file, so the documentation describes the second parameter as the archive - ancestor supplied by the caller rather than naming the global. Record - the run in + asserts that the token is absent from this file both before and after the edit, so the totality + claim is worded as "returns a value for every input and propagates no exception". The token + `Globals.Ol.ArchiveRootPath` is barred as well and for an additional reason: the helper takes its + archive ancestor as a parameter and must never name the global, because doing so would reopen the + unguarded-read defect issue #638 closed. The documentation therefore describes the second + parameter as the archive ancestor supplied by the caller. Record the run in `evidence/regression-testing/p4-t1-helper-implemented.md`, capturing the output of - `rg -n "throw" QuickFiler/Controllers/EfcDataModel.cs` taken immediately before and immediately - after the edit. Acceptance: the two `rg` outputs contain the identical set of matched line - **texts**, compared without their line numbers because the helper body changes length and shifts - every later line number, so the helper introduces no new throw site; - `rg -n "IsFullOutlookPath" QuickFiler/Controllers/EfcDataModel.cs` returns exactly 1 line and it - is inside the helper, where before this task it returned 0 lines; and - `(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.cs").Count` is at most 500. + `rg -n "throw" QuickFiler/Controllers/EfcDataModel.FilingStem.cs` taken immediately before and + immediately after the edit, each recorded with `ExpectedExitCode: 1` because ripgrep exits 1 on + zero matches. Acceptance: both `rg` invocations report zero matches and exit non-zero, so the + helper introduces no throw site and none was present in the seam; + `rg -n "IsFullOutlookPath" QuickFiler/Controllers/EfcDataModel.FilingStem.cs` returns exactly 1 + line and it is inside the helper, where before this task it returned 0 lines; + `rg -n "Globals" QuickFiler/Controllers/EfcDataModel.FilingStem.cs` returns 0 lines, recorded + with `ExpectedExitCode: 1`; + `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.FilingStem.cs").Count'` + is at most 500; and + `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.cs").Count'` + reports exactly 485, because this task changes no line of that file. - [ ] [P4-T2] Record the helper's line range and verify its purity, and write `evidence/regression-testing/p4-t2-helper-shape.md`. Acceptance: the artifact records the first - and last line numbers of the `ToFilingStemOrVerbatim` declaration body; it records that the body + and last line numbers of the `ToFilingStemOrVerbatim` declaration body **in + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`**, and records separately that + `QuickFiler/Controllers/EfcDataModel.cs` contains no part of the helper, so a reader cannot + mistake one file's line numbers for the other's; it records that the body contains no `await`, no `Globals`, no `logger`, and no `throw`; it records that the only call - sites of the helper are the single assignment in the `string` overload and the eight tests in + sites of the helper are the single assignment at `EfcDataModel.cs:337` and the eight tests in `EfcDataModelIssue637Tests`, verified by `rg -n "ToFilingStemOrVerbatim" --glob "*.cs" .`; and it enumerates every line `rg -n "ToFilingStemOrVerbatim" --glob "*.cs" QuickFiler/` returns and classifies each as the single declaration, the single call, or an XML-documentation reference, @@ -762,35 +1010,49 @@ failure. `EXIT_CODE: 0`; both outputs contain `(Rebuild target(s))`; and neither recorded `Command:` line contains the solution-wide nullable opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. -- [ ] [P4-T4] Run the helper test class green. Use the P2-T8 command with the results directory +- [ ] [P4-T4] Run the helper test class green. Use the P2-T12 command with the results directory changed to `coverage\testresults\p4-t4`, and write `evidence/regression-testing/p4-t4-helper-tests-green.md`. Acceptance: `EXIT_CODE: 0`; 8 tests - total; 8 passed; 0 failed; and the two tests that failed in P2-T8 are named individually as now + total; 8 passed; 0 failed; and the two tests that failed in P2-T12 are named individually as now passing. - [ ] [P4-T5] Prove the eight existing `ToArchiveRelativeStem` tests are unchanged and still pass. Use - the P2-T7 command with the filter + the P2-T11 command with the filter `"/TestCaseFilter:FullyQualifiedName~EfcDataModelIssue614Tests&TestCategory!=LiveOutlook"` and the results directory `coverage\testresults\p4-t5`, and write `evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md`. Acceptance: `EXIT_CODE: 0`; the run reports 8 tests for that class; 8 passed including `ToArchiveRelativeStem_ArchiveRootItself_Throws`; and, after `git add QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, - `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` + `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` shows zero removed content lines. - [ ] [P4-T6] Verify AC17: the non-goals are untouched. Run `git add QuickFiler/Controllers/EfcDataModel.cs` then - `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -U0 -- QuickFiler/Controllers/EfcDataModel.cs` - and write `evidence/regression-testing/p4-t6-nongoals-untouched.md`. Acceptance: no hunk header - addresses any line inside the original ranges 299 to 334 (`OpenOlFolderAsync` and - `OpenFsFolderAsync`) or 336 to 386 (the `MAPIFolder` overload and `ToArchiveRelativeStem`); a - pure-insertion hunk whose old-side range is `-297,0` or `-298,0` is the helper declaration - required by P2-T1 and is expected; any other hunk outside the line-287 assignment fails this - task; and `rg -n "Globals.Ol.ArchiveRootPath" QuickFiler/Controllers/EfcDataModel.cs` returns - exactly 4 lines, quoted in the artifact and classified as the 3 pre-existing `OlAncestor` - initializers (originally lines 289, 310 and 328, shifted by the length of the helper this plan - adds) plus the single new argument on the `DestinationOlStem` assignment introduced by P2-T1; - and none of the 4 is inside a `try` or `catch` block, verified by quoting the enclosing - statement of each. + `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -U0 -- QuickFiler/Controllers/EfcDataModel.cs` + and write `evidence/regression-testing/p4-t6-nongoals-untouched.md`, quoting every hunk header + verbatim. This plan makes exactly two edits to this file, both single-line substitutions, so the + file's line count and every line number in it are unchanged and the diff is exactly two hunks. + Acceptance, in five parts. + First, the diff contains exactly two hunk headers: one addressing old line 21 only — the + `partial` keyword added by P2-T1, accepted in either the `-21 +21` or the `-21,1 +21,1` spelling + git may emit under `-U0` — and one addressing old line 337 only, the `DestinationOlStem` + assignment redirected by P2-T4. Any third hunk, and any hunk whose old-side range spans more + than one line, fails this task. + Second, no hunk header addresses any line inside the protected range 271 to 297. That range is + `TryGetArchiveRoot`, introduced by issue #638 with its declaration at 280; it is that issue's + code and this plan must not touch it. + Third, no hunk header addresses any line inside 349 to 396 (`OpenOlFolderAsync` at 349-372 and + `OpenFsFolderAsync` at 374-396) or inside 398 to 448 (the `MAPIFolder` overload at 398-419 and + `ToArchiveRelativeStem` at 421-448). + Fourth, `rg -n "Globals.Ol.ArchiveRootPath" QuickFiler/Controllers/EfcDataModel.cs` returns + **exactly 1** line, and it is line 284. The artifact quotes it and classifies it as the single + guarded read inside `TryGetArchiveRoot`. There is no second read to classify: issue #638 + replaced the three former `OlAncestor` initializer reads with the `out var olAncestor` local + produced by `TryGetArchiveRoot`, and P2-T4 is required to pass that same local rather than the + global. + Fifth, the artifact records the inverse assertion that actually protects the non-goal: the single + read at line 284 remains inside the `try` block at 282-286, whose `catch (InvalidOperationException ex)` + is at 287, exactly as issue #638 wrote it, and no read is added, removed, or moved into or out of + that block. The artifact quotes lines 280 through 297 verbatim to evidence this. ### Phase 5 — Change C, the recorded spec correction to the issue #439 assertion @@ -820,21 +1082,27 @@ failure. ` // Arrange: the presented target is rooted with casing different from the configured` and ` // root, so the provider must receive the original full path unchanged (#439).` - Acceptance: - `rg -n "so the provider must receive the original full path unchanged" QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` - returns exactly 1 line, and + The asserted token below is the replacement's own distinguishing suffix rather than the phrase + the two versions share. Line 122 already reads + ` // root, so the provider must receive the original full path unchanged.` before this + task runs, so a search for that shared phrase returns 1 line before any edit and cannot fail. The + literal `unchanged (#439).` is absent from the tracked tree until this task writes it and is + quoted here verbatim for that reason; it is asserted with `-F` because the parentheses and the + period are regex metacharacters that must match those same characters. Acceptance: + `rg -F -n 'unchanged (#439).' QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` + returns exactly 1 line and it is line 122, and `rg -n "already rooted with casing different" QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` - returns 0 lines. + returns 0 lines, recorded with `ExpectedExitCode: 1`. - [ ] [P5-T4] Verify AC19: the companion provider assertion and `ToHierarchyPath` are preserved. Write `evidence/regression-testing/p5-t4-provider-assertion-preserved.md`. Acceptance: lines 161 to 164 of `BreadcrumbBridgeRouterIssue439Tests.cs` are byte-identical to their pre-change text, quoted in the artifact; and, after `git add QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`, - `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` + `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` produces no output at all, since this plan changes no line of that file. - [ ] [P5-T5] Verify the file did not grow and that exactly one assertion changed. Run `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs").Count'`, then `git add QuickFiler.Test` followed by - `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- QuickFiler.Test`, and write + `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- QuickFiler.Test`, and write `evidence/regression-testing/p5-t5-single-assertion-change.md`. Acceptance: the line count is exactly 694; among the diff's removed content lines, exactly one matches `.Should()`, and it is ` router.SelectedFolderPath.Should().Be(fullTarget);`; and the artifact records the @@ -849,7 +1117,7 @@ failure. `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`; record its `EXIT_CODE:` and its `(Rebuild target(s))` line in the same artifact. Without this rebuild the scoped run would execute the assembly P4-T3 produced, which still carries the old method name and the old - assertion, and its acceptance would be unsatisfiable. Then use the P2-T7 command with the filter + assertion, and its acceptance would be unsatisfiable. Then use the P2-T11 command with the filter `"/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue439Tests&TestCategory!=LiveOutlook"` and the results directory `coverage\testresults\p5-t6`, and write `evidence/regression-testing/p5-t6-issue439-green.md`. Acceptance: `EXIT_CODE: 0`; 0 failed; and @@ -881,7 +1149,7 @@ failure. `(Rebuild target(s))` line in the same artifact. Without this rebuild the scoped run would execute the assembly P4-T3 produced, which predates those edits, so the run would not be evidence about the edited file that AC23 requires. Then run - `rg -c "deferred to issue #637" --glob "*.cs" .`, then run the P2-T7 command with the filter + `rg -c "deferred to issue #637" --glob "*.cs" .`, then run the P2-T11 command with the filter `"/TestCaseFilter:FullyQualifiedName~EfcSelectionGuardTests&TestCategory!=LiveOutlook"` and the results directory `coverage\testresults\p6-t4`, and write `evidence/regression-testing/p6-t4-deferral-cleared.md`. The `*.cs` glob is load-bearing: the @@ -891,26 +1159,54 @@ failure. `EXIT_CODE: 0` with 0 failed; and the artifact records that `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` passed and that neither `IsValidFilingSelection` nor `IsValidCreationSelection` had any executable line changed, verified - by a `git diff ecdb1c84ba8541ab67042985919cfed4df768c01 --cached -- QuickFiler/Controllers/EfcSelectionGuard.cs` + by a `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- QuickFiler/Controllers/EfcSelectionGuard.cs` run in the same task after `git add QuickFiler/Controllers/EfcSelectionGuard.cs`, whose only changed line is line 30. -- [ ] [P6-T5] Commit changes A through D. Run +- [ ] [P6-T5] Redact host identity from every evidence artifact written so far, then prove it. Apply + the "Evidence transcript redaction" convention to every file already written under + `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/`: + replace every occurrence of the absolute worktree path with the literal ``, and + every remaining absolute path beginning with the Windows per-user profile root with + ``, and every vstest results file name with ``. Then run two searches. + Search 1: + `rg -F -n 'C:\Users' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**' --glob '!**/p6-t5-evidence-redaction.md'`. + Search 2: + `rg -F -n '.trx' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**' --glob '!**/p6-t5-evidence-redaction.md'`. + Write `evidence/other/p6-t5-evidence-redaction.md` with `ExpectedExitCode: 1`. Acceptance: + both searches report 0 matches and exit non-zero; the artifact lists every evidence file it + rewrote and, for each, the number of replacements made; the artifact records that a zero-match + result over the two-component prefix `C:\Users` proves the account segment is absent as well, + because on Windows that segment always follows the prefix immediately; and it records that + Search 2 covers the vstest results file name, which carries the account and the machine with no + preceding profile path and so is invisible to Search 1. The `-F` flag is required in both + because the backslash and the period are regex metacharacters that must match themselves. The + `--glob` exclusion of + this task's own artifact is required for the same reason P7-T10 excludes its own: this artifact + records its own two `Command:` lines, and those commands' patterns are the strings being searched + for. No other evidence file of this feature is excluded. +- [ ] [P6-T6] Commit changes A through D. Run `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` then `git commit -m "fix(637): normalize the breadcrumb producer and the string filing overload"` and - write `evidence/other/p6-t5-commit.md`. A commit is required here because every Phase 7 and - Phase 8 gate is anchored to `ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD`, and an anchored diff + write `evidence/other/p6-t6-commit.md`. A commit is required here because every Phase 7 and + Phase 8 gate is anchored to `b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD`, and an anchored diff reports nothing for changes that are not yet committed. Acceptance: `EXIT_CODE: 0`; `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and - `git diff --name-only ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- QuickFiler QuickFiler.Test` - lists exactly these eight paths and no others: + `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler QuickFiler.Test` + lists exactly these ten paths and no others: `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`, - `QuickFiler/Controllers/EfcDataModel.cs`, `QuickFiler/Controllers/EfcSelectionGuard.cs`, + `QuickFiler/Controllers/EfcDataModel.cs`, + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, + `QuickFiler/Controllers/EfcSelectionGuard.cs`, + `QuickFiler/QuickFiler.csproj`, `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`, `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs`, `QuickFiler.Test/QuickFiler.Test.csproj`. + The enumeration is ten rather than the eight this plan named before the change-B file split: the + split adds `QuickFiler/Controllers/EfcDataModel.FilingStem.cs` and the + `QuickFiler/QuickFiler.csproj` registration that makes it compile. ### Phase 7 — Final QC toolchain loop and coverage delta @@ -937,7 +1233,7 @@ again from the start. the write-mode discrimination that a read-only command cannot supply is provided by P7-T1's before-and-after porcelain pair. Then, in this same task, run `git add QuickFiler QuickFiler.Test` and `git commit -m "style(637): apply csharpier formatting before the coverage gates"`, so that - every subsequent `ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD` diff describes the same file + every subsequent `b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD` diff describes the same file contents the P7-T5 build measured. If nothing changed, record that the commit was a no-op and that the tree already matched `HEAD`. Record the commit result in `evidence/qa-gates/p7-t2-csharpier-check.md`. @@ -964,31 +1260,43 @@ again from the start. passed in the baseline is failing now; the artifact names every baseline failure that is still failing; and when `BASELINE_FAILURE_SET` is empty, `EXIT_CODE: 0` and 0 failed are required. `Output Summary:` additionally carries the six numeric `/coverage` attribute values and the - derived line and branch percentages that P7-T6 reads, copied in once P7-T6 has produced them; - this task is not complete until that copy-back has been made, because the plan contract - requires the final-QC test-step artifact itself to carry the numeric coverage headline. + derived line and branch percentages that P7-T6 reads, copied in once P7-T6 has produced them, + because the plan contract requires the final-QC test-step artifact itself to carry the numeric + coverage headline. The execution order for the pair is explicit and is not a deadlock: run this + task's command and write everything except the copied-back headline, leave P7-T5 **unchecked**, + execute P7-T6 in full, then return to P7-T5, write the copy-back into `Output Summary:`, and + check both tasks off together. - [ ] [P7-T6] Read the post-change numeric coverage headline. Run the P0-T16 command with the input path changed to `.\coverage\p7-t5-postchange.cobertura.xml` and write `evidence/qa-gates/p7-t6-coverage-headline.md`. Acceptance: `EXIT_CODE: 0`, and `Output Summary:` records all six numeric attribute values plus the derived line-coverage percentage and branch percentage. - [ ] [P7-T7] Verify changed-line coverage. Run the P0-T17 command with the input path changed to - `.\coverage\p7-t5-postchange.cobertura.xml`, and in the same task run - `git diff -U0 ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/EfcDataModel.cs` - to enumerate the added line numbers from the hunk headers. Re-derive the + `.\coverage\p7-t5-postchange.cobertura.xml` and with its file list extended to the three + production files this plan touches — `QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs`, + `QuickFiler\Controllers\EfcDataModel.cs` and + `QuickFiler\Controllers\EfcDataModel.FilingStem.cs` — and in the same task run + `git diff -U0 b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/EfcDataModel.cs QuickFiler/Controllers/EfcDataModel.FilingStem.cs` + to enumerate the added line numbers from the hunk headers. The base anchor + `b9476588e0e49e113c73cc55cc918f4a65e022fd` is the post-merge, pre-change baseline P0-T8 proved + clean, so the added-line set this diff produces contains only lines this plan added and none of + issue #638's. Re-derive the `ToFilingStemOrVerbatim` line range against the post-format working tree in this same task, recording the declaration line and the closing-brace line, and record both that range and the range `evidence/regression-testing/p4-t2-helper-shape.md` recorded, stating whether they differ. Every coverage assertion in this task is evaluated against the re-derived range; the P4-T2 range - is recorded for audit only. This re-derivation is required because P4-T2 measured the range in - Phase 4, P7-T1 then ran the write-mode formatter over `EfcDataModel.cs` — the first format pass - over the hand-written helper body, since Phases 2 through 6 contain no format step — and P7-T5 - measured the tree after it, so a formatter change to the helper's extent would make the P4-T2 - range identify uncovered lines in `OpenOlFolderAsync` or unrelated covered lines instead of the - helper. Write + is recorded for audit only. The range is re-derived against + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, which is the file that declares the helper; + `QuickFiler/Controllers/EfcDataModel.cs` declares no part of it. This re-derivation is required + because P4-T2 measured the range in + Phase 4, P7-T1 then ran the write-mode formatter over `EfcDataModel.FilingStem.cs` — the first + format pass over the hand-written helper body, since Phases 2 through 6 contain no format step — + and P7-T5 measured the tree after it, so a formatter change to the helper's extent would make the + P4-T2 range identify the wrong lines. Write `evidence/qa-gates/p7-t7-changed-line-coverage.md`. Acceptance: the artifact lists, per file, the set of added line numbers and the set of line numbers with zero hits; the intersection of those - two sets is empty for both files; and, for `QuickFiler/Controllers/EfcDataModel.cs`, every line + two sets is empty for all three files; and, for + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, every line number inside the re-derived `ToFilingStemOrVerbatim` range has non-zero hits, which is the new-code coverage requirement for the new helper stated in AC29; and, for the re-derived `ToFilingStemOrVerbatim` range, the artifact records the line nodes carrying `branch="True"` @@ -1014,27 +1322,40 @@ again from the start. three labelled numeric sections: baseline coverage, copied from `evidence/baseline/p0-t16-coverage-headline.md`; post-change coverage, copied from `evidence/qa-gates/p7-t6-coverage-headline.md`; and changed and new-code coverage, copied from - `evidence/qa-gates/p7-t7-changed-line-coverage.md`. Acceptance: all three sections carry numeric + `evidence/qa-gates/p7-t7-changed-line-coverage.md`. The artifact also states the coverage-floor + authority resolution this plan applies, in these terms: `CLAUDE.md` states a repository-wide floor + of at or above 80 percent line coverage on the testable denominator, together with a + maintainer-ratified COM/VSTO/WinForms exemption; `.claude/rules/general-unit-test.md` states 85 + percent line and 75 percent branch; `policy-compliance-order` ranks `CLAUDE.md` first and + `.claude/rules/general-unit-test.md` third, so `CLAUDE.md`'s 80 percent floor is the binding + repository-wide gate here and the 85/75 figures are the stricter non-binding target, superseded + where they conflict. The binding figure is also the one the runner enforces, at + `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487`, so the gate this task applies and the + gate the tooling applies are the same number. Acceptance: all three sections carry numeric values and none carries a placeholder; the post-change line-coverage percentage is at or above - 80, which is the floor the coverage runner itself enforces at - `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487-489` and which CLAUDE.md states, except - that if the baseline figure recorded in `evidence/baseline/p0-t16-coverage-headline.md` is - itself already below 80 the artifact records `BASELINE BELOW FLOOR`, reports that pre-existing - condition to the orchestrator, and the binding requirement becomes that the post-change figure is - at or above the recorded baseline figure; the - artifact records the post-change figure against the 85 percent line and 75 percent branch figures - in `.claude/rules/general-unit-test.md` and states explicitly which of the two repository-wide - figures each threshold comes from, without resolving the conflict; and the changed-line section - records an empty uncovered intersection. + **80**, and that clause is blocking; the artifact carries the authority statement above with its + two document citations; the artifact additionally records the post-change line and branch + percentages against the 85 percent and 75 percent figures as an explicitly **non-blocking** + observation, labelled `NON-BLOCKING TARGET:`, so the gap stays visible without gating the work; + and the changed-line section records an empty uncovered intersection. One exception applies to + the blocking clause and to nothing else: if the baseline figure recorded in + `evidence/baseline/p0-t16-coverage-headline.md` is itself already below 80, the artifact records + `BASELINE BELOW FLOOR`, reports that pre-existing condition to the orchestrator, and the binding + requirement becomes that the post-change figure is at or above the recorded baseline figure. The + change-scoped gates — no changed line loses coverage, and every line of the new helper is covered + — remain blocking in every case, including under that exception. - [ ] [P7-T9] File-size audit, run after the formatter rather than before it, because CSharpier can change a file's line count. Run - `pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcSelectionGuard.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs","QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs","QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }'` - and write `evidence/qa-gates/p7-t9-file-sizes.md`. Acceptance: `EfcDataModel.cs` is at most 500; + `pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\EfcDataModel.FilingStem.cs","QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcSelectionGuard.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs","QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs","QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }'` + and write `evidence/qa-gates/p7-t9-file-sizes.md`. Acceptance: `EfcDataModel.cs` is at most 500, + with the exact value recorded, and the artifact states whether it is still 485, which is the + value the plan expects because this plan makes only single-line substitutions in it; + `EfcDataModel.FilingStem.cs` is at most 500, with the exact value recorded; `BreadcrumbBridgeRouterIssue637Tests.cs` is at most 500; `EfcDataModelIssue614Tests.cs` is at most 500; `BreadcrumbBridgeRouterIssue439Tests.cs` is at most 694 and therefore has not grown, with the exact value recorded; `EfcSelectionGuard.cs` is at most 79, with the exact value recorded; `EfcSelectionGuardTests.cs` is at most 296, with the exact value recorded; and - `BreadcrumbBridgeRouter.Selection.cs` is at most 500. The three upper bounds replace exact + `BreadcrumbBridgeRouter.Selection.cs` is at most 500. The upper bounds replace exact equalities because this task runs after a write-mode formatter that can reduce a line count, and AC25 requires only that these files not grow. - [ ] [P7-T10] Toolchain non-vacuity audit. Write `evidence/qa-gates/p7-t10-toolchain-audit.md` @@ -1047,9 +1368,11 @@ again from the start. returning 0 matches and exiting non-zero, recorded with `ExpectedExitCode: 1` for that step; and both MSBuild transcripts contain `(Rebuild target(s))`. Three scope restrictions on that command are load-bearing. The directory operand must be this feature's folder rather than - `docs/features/active`, because 121 evidence Markdown files under other feature folders in that + `docs/features/active`, because many evidence Markdown files under other feature folders in that tree contain the token and this plan cannot change them, so the parent-directory form can never - return 0 and the gate could never pass. The restriction to the evidence subtree is required + return 0 and the gate could never pass. No count of those files is stated here deliberately: the + count changes as other feature work lands, and this justification does not depend on its value. + The restriction to the evidence subtree is required because `spec.md` and this plan both discuss the token in prose and both live at the root of this feature's folder, so a scan of the folder without the `**/evidence/**/*.md` glob would never return 0 either. The exclusion of `p7-t10-toolchain-audit.md` is required because this @@ -1077,11 +1400,35 @@ again from the start. `dotnet-coverage --output-format cobertura` rather than by `/EnableCodeCoverage`, which is the repository's standard runner and the local analogue of `.github/workflows/_mstest-coverage.yml:83`. The substitutions are recorded, not resolved. -- [ ] [P7-T11] Commit the QA evidence and any residual formatting result. Run +- [ ] [P7-T11] Redact host identity from the Phase 7 evidence artifacts, then prove it. Apply the + "Evidence transcript redaction" convention to every file written under + `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/` + since P6-T5 ran: replace every occurrence of the absolute worktree path with the literal + ``, and every remaining absolute path beginning with the Windows per-user profile + root with ``, and every vstest results file name with ``. The Phase 7 + artifacts are the ones most exposed to this, because + P7-T3 and P7-T4 record MSBuild transcripts and MSBuild prints an absolute project path for every + project it builds. Then run two searches. Search 1: + `rg -F -n 'C:\Users' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**' --glob '!**/p7-t11-evidence-redaction.md' --glob '!**/p6-t5-evidence-redaction.md'`. + Search 2: + `rg -F -n '.trx' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**' --glob '!**/p7-t11-evidence-redaction.md' --glob '!**/p6-t5-evidence-redaction.md'`. + Write `evidence/qa-gates/p7-t11-evidence-redaction.md` with `ExpectedExitCode: 1`. + Acceptance: both searches report 0 matches and exit non-zero; the artifact lists every evidence + file it rewrote and, for each, the number of replacements made; and the artifact records that the + scan covers the whole feature evidence tree, so it re-verifies the Phase 0 through Phase 6 + artifacts P6-T5 cleared as well as the Phase 7 artifacts. The `-F` flag is required in both + because the backslash and the period are regex metacharacters that must match themselves. + Exactly two evidence artifacts are + excluded, and both for the same reason P7-T10 excludes its own: each records its own `Command:` + lines, and those commands' patterns are the strings being searched for. They are this task's + artifact and `evidence/other/p6-t5-evidence-redaction.md`, which P6-T5 wrote earlier in the same + feature tree. Omitting the second exclusion would make this gate unsatisfiable. No other evidence + file of this feature is excluded. +- [ ] [P7-T12] Commit the QA evidence and any residual formatting result. Run `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` then `git commit -m "chore(637): final QC toolchain pass and coverage evidence"` and write - `evidence/other/p7-t11-commit.md`. Acceptance: `EXIT_CODE: 0`, and + `evidence/other/p7-t12-commit.md`. Acceptance: `EXIT_CODE: 0`, and `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and `git status --porcelain -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` lists at most this task's own evidence @@ -1100,7 +1447,7 @@ is checked off before its cited evidence exists. Exactly one criterion is checke - [ ] [P8-T1] AC1: cite `evidence/regression-testing/p3-t5-router-tests-green.md` showing `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected` and `RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection` passing, and - `evidence/regression-testing/p2-t7-router-tests-red.md` showing both failing before the fix. + `evidence/regression-testing/p2-t11-router-tests-red.md` showing both failing before the fix. Acceptance: both artifacts exist and name both tests; AC1 is checked off. - [ ] [P8-T2] AC2: cite `evidence/regression-testing/p3-t5-router-tests-green.md` showing `RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` and @@ -1120,7 +1467,11 @@ is checked off before its cited evidence exists. Exactly one criterion is checke `RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim`, and `evidence/regression-testing/p4-t4-helper-tests-green.md` for `ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim`. Acceptance: both artifacts exist - and name the tests; AC5 is checked off. + and name the tests; the check-off record additionally states that AC5's citation of + `EfcDataModel.cs:272` for the `folderpath != "Trash to Delete"` comparison is stale — the + comparison is at line **316** on the merged tree — that the discrepancy is recorded in full by + P8-T32, and that AC5's binding clause, which is behavioral rather than positional, is + unaffected; AC5 is checked off. - [ ] [P8-T6] AC6: cite `evidence/regression-testing/p3-t2-nesting.md` for the preserved message literal, `evidence/regression-testing/p3-t5-router-tests-green.md` for `RowSelected_OutOfRootRootedTarget_IsStillRejected` and @@ -1146,9 +1497,18 @@ is checked off before its cited evidence exists. Exactly one criterion is checke than through the `rowSelected` inbound message. Acceptance: the artifact names that test as passing; AC10 is checked off. - [ ] [P8-T11] AC11: cite `evidence/regression-testing/p4-t2-helper-shape.md` for the single - `internal static` declaration, the single assignment call site, and the purity record, and + `internal static` declaration in `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, the single + assignment call site at `QuickFiler/Controllers/EfcDataModel.cs:337`, and the purity record, and `evidence/regression-testing/p4-t4-helper-tests-green.md` for the eight tests that invoke the - helper directly without constructing an `EmailFiler`. Acceptance: both artifacts exist; AC11 is + helper directly without constructing an `EmailFiler`. Also cite + `evidence/regression-testing/p2-t13-compile-include-observed.md` and + `evidence/other/p6-t6-commit.md` for the two supporting edits the file split requires: the + `partial` keyword on `EfcDataModel.cs:21` and the `` registration in + `QuickFiler/QuickFiler.csproj`. Acceptance: all four artifacts exist; the check-off record states + that AC11's text was corrected in `spec.md` to name the new declaring file and the corrected + assignment line 337, that the helper remains the member + `EfcDataModel.ToFilingStemOrVerbatim` because the new file is a partial of the same type, and + that the split is the remedy `spec.md:414-416` authorizes for the 15-line headroom; AC11 is checked off. - [ ] [P8-T12] AC12: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for `ToFilingStemOrVerbatim_RelativeStem_ReturnsTheInputVerbatim` and @@ -1157,7 +1517,7 @@ is checked off before its cited evidence exists. Exactly one criterion is checke - [ ] [P8-T13] AC13: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` and `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem`, together with - `evidence/regression-testing/p2-t8-helper-tests-red.md` showing both failing before the fix. + `evidence/regression-testing/p2-t12-helper-tests-red.md` showing both failing before the fix. Acceptance: both artifacts exist and name both tests; AC13 is checked off. - [ ] [P8-T14] AC14: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for `ToFilingStemOrVerbatim_ArchiveRootExact_ReturnsTheInputVerbatimAndDoesNotThrow`, @@ -1171,16 +1531,29 @@ is checked off before its cited evidence exists. Exactly one criterion is checke `evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md` for the zero-removed-line diff and the 8 passing results including `ToArchiveRelativeStem_ArchiveRootItself_Throws`, and `evidence/regression-testing/p4-t6-nongoals-untouched.md` for the unmodified `MAPIFolder` - overload and its call at the original line 345. Acceptance: all three artifacts exist; AC15 is - checked off. + overload at `EfcDataModel.cs:398-419` and its call to `ToArchiveRelativeStem` at line **407**. + Acceptance: all three artifacts exist; the check-off record states that AC15's citations were + corrected in `spec.md` from `:372-386`, `:336-357` and `:345` to the measured + `:421-448`, `:398-419` and `:407`; AC15 is checked off. - [ ] [P8-T16] AC16: re-run both P1-T2 searches against the post-change tree and write `evidence/qa-gates/p8-t16-movetofolder-family-post.md`. Acceptance: the syntax-anchored search - still returns exactly 9 lines classified as 3 declarations and 6 call sites, the stem search - still returns 16 lines across 5 files, and no new overload and no signature change appears; AC16 - is checked off. + still returns exactly **10** lines across **5** files, classified as 3 declarations and 7 call + sites; the stem search still returns **23** lines across **6** files; no new overload and no + signature change appears; the check-off record states that AC16's counts and citations were + corrected in `spec.md` to those measured figures, and that the pre-merge figure of 16 lines is + superseded; AC16 is checked off. - [ ] [P8-T17] AC17: cite `evidence/regression-testing/p4-t6-nongoals-untouched.md`. Acceptance: the - artifact shows no hunk in the original ranges 299 to 334 and 336 to 386 and records that no - `Globals.Ol.ArchiveRootPath` read gained a `try` or `catch`; AC17 is checked off. + artifact shows no hunk in the ranges 349 to 396 (`OpenOlFolderAsync` and `OpenFsFolderAsync`) or + 398 to 448 (the `MAPIFolder` overload and `ToArchiveRelativeStem`), and no hunk in the protected + range 271 to 297; it records that `Globals.Ol.ArchiveRootPath` occurs exactly once in the file, + at line 284; and it records that the guarded read at 284 and the + `UserDiagnosticAction(ArchiveRootUnavailableMessage)` degrade at 358 and 382, all introduced by + issue #638, are preserved unchanged. The check-off record states that AC17 was reworded in + `spec.md` for this reason: its original clause required that no `Globals.Ol.ArchiveRootPath` read + gains a try/catch or a degrade, and issue #638 had already given the file's single read both, so + that clause was false on the merged tree before this plan ran and no action this plan authorizes + could make it true. The reworded clause requires instead that #638's guarded read and degrade are + preserved unchanged, which is the property this plan can and does deliver; AC17 is checked off. - [ ] [P8-T18] AC18: cite `evidence/regression-testing/p5-t5-single-assertion-change.md`, which records all three clauses. Acceptance: the artifact records the corrected assertion at line 165, the renamed method, and the narrowed two-line comment; AC18 is checked off. @@ -1213,13 +1586,18 @@ is checked off before its cited evidence exists. Exactly one criterion is checke still 9 lines across 3 files with 2 writes and 3 reads, no new write site appears, no new public API member appears, and `rg -n "public string\? SelectedFolderPath \{ get; private set; \}" QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` returns exactly 1 line; AC24 is checked off. -- [ ] [P8-T25] AC25: cite `evidence/qa-gates/p7-t9-file-sizes.md`. Acceptance: the artifact shows every - listed file at or under 500 lines, `BreadcrumbBridgeRouterIssue439Tests.cs` at or under 694 and - therefore not grown, and it records that the spec's stated 424 for `EfcDataModel.cs` was 423 on - the tree before the change; AC25 is checked off. The bound is stated as "at or under" rather +- [ ] [P8-T25] AC25: cite `evidence/qa-gates/p7-t9-file-sizes.md` and + `evidence/baseline/p1-t7-file-line-counts.md`. Acceptance: the artifact shows every + listed file at or under 500 lines, including the new + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`; `BreadcrumbBridgeRouterIssue439Tests.cs` at + or under 694 and therefore not grown; and the check-off record states that `EfcDataModel.cs` was + **485** lines before the change, not the 424 the spec's implementation table and AC25's + parenthetical stated, that AC25's parenthetical was corrected to 485 in `spec.md`, that the + resulting headroom of 15 lines is what forced the change-B file split, and that AC25 was extended + in `spec.md` to name the new file; AC25 is checked off. The bound is stated as "at or under" rather than "exactly" for the same reason it is in P7-T9: the figure is read after a write-mode formatter that can reduce a line count, and AC25 requires only non-growth. -- [ ] [P8-T26] AC26: cite `evidence/regression-testing/p2-t9-compile-include-observed.md` for the +- [ ] [P8-T26] AC26: cite `evidence/regression-testing/p2-t13-compile-include-observed.md` for the `Compile Include` line and the 10 observed test results, and `evidence/regression-testing/p3-t5-router-tests-green.md` for the same 10 tests executing after the fix. Acceptance: both artifacts exist and both record 10 executed tests; AC26 is checked off. @@ -1261,8 +1639,14 @@ is checked off before its cited evidence exists. Exactly one criterion is checke against the pre-format range `evidence/regression-testing/p4-t2-helper-shape.md` recorded, with P7-T7's record of whether the two ranges differ cited here; the artifact `evidence/qa-gates/p7-t8-coverage-delta.md` records either a post-change line-coverage - percentage at or above 80 or an explicit `BASELINE BELOW FLOOR` finding with the post-change - figure at or above the recorded baseline; the `IsFullOutlookPath` conditional in the new helper + percentage at or above 80 — the binding repository-wide floor under the authority resolution + P7-T8 states, in which `policy-compliance-order` ranks `CLAUDE.md` above + `.claude/rules/general-unit-test.md`, so the latter's 85 and 75 figures are the non-binding + target and are recorded as such — or an explicit `BASELINE BELOW FLOOR` finding with the + post-change figure at or above the recorded baseline; the changed-line intersection is empty for + all three production files, including the new + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`; the `IsFullOutlookPath` conditional in the + new helper shows both branches taken, per the `condition-coverage` values P7-T7 recorded, or — when P7-T7 records that the helper's range carries no `branch="True"` node — per the two witness tests P7-T7 names; and AC29 is checked off. @@ -1270,15 +1654,15 @@ is checked off before its cited evidence exists. Exactly one criterion is checke this task, in this order. First the porcelain companion, `git status --porcelain -- QuickFiler QuickFiler.Test UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. Second - `git diff --name-only ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- QuickFiler QuickFiler.Test`. + `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler QuickFiler.Test`. Third - `git diff --name-only ecdb1c84ba8541ab67042985919cfed4df768c01..HEAD -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. + `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. Write `evidence/qa-gates/p8-t30-scope-boundary.md` recording all three outputs verbatim. The porcelain companion is required because a name-listing diff enumerates tracked changes only and never reports an untracked path, so the two diffs alone cannot fail on a file this plan created and left uncommitted. At the point this task runs the division of labour between the two - mechanisms is fixed and is stated here: P6-T5 committed changes A through D, P7-T2 committed the - formatting result, and P7-T11 committed the Phase 7 evidence, so both anchored diffs carry the + mechanisms is fixed and is stated here: P6-T6 committed changes A through D, P7-T2 committed the + formatting result, and P7-T12 committed the Phase 7 evidence, so both anchored diffs carry the enumeration assertion, and the porcelain span is expected to be empty because every path it covers is already in `HEAD`. That emptiness is itself the assertion and not a null result — an untracked or unstaged file anywhere in those @@ -1288,11 +1672,11 @@ is checked off before its cited evidence exists. Exactly one criterion is checke omits `docs/features/active` deliberately: Phase 8 writes evidence artifacts and edits `spec.md` under that path and P8-T33 commits them afterwards, so a porcelain span covering it would be non-empty for reasons this gate is not measuring. Acceptance: the porcelain invocation produces - no output; the second command lists the eight paths enumerated in P6-T5, plus — only when the + no output; the second command lists the ten paths enumerated in P6-T6, plus — only when the `BASELINE_FORMAT_DRIFT` section of `evidence/baseline/p0-t12-csharpier-check.md` is non-empty — the paths in that section that lie under `QuickFiler` or `QuickFiler.Test`, each of which the artifact must show as a formatting-only change committed by P7-T2, and no others; when - `BASELINE_FORMAT_DRIFT` is empty the list is exactly the eight paths; the third command + `BASELINE_FORMAT_DRIFT` is empty the list is exactly the ten paths; the third command produces no output, which is the evidence that `UtilitiesCS`, `TaskMaster`, `ToDoModel`, `Tags`, `TaskVisualization`, `UtilitiesCS.Test` and `TaskMaster.Test` contain no changed file; and AC30 is checked off. @@ -1308,13 +1692,62 @@ is checked off before its cited evidence exists. Exactly one criterion is checke and 0 unchecked; both agree; and the artifact records that an unscoped count of every `- [x]` and `- [ ]` line in `spec.md` would over-report by exactly 5, naming those five line numbers. - [ ] [P8-T32] Record the spec-versus-tree discrepancies found during this work in - `evidence/other/p8-t32-spec-tree-discrepancies.md`. Acceptance: the artifact records the three - items listed under "Tree observations recorded while authoring this plan" that concern `spec.md` - citations — the 423-versus-424 line count for `EfcDataModel.cs`, the `:167-214`-versus-`:167-213` - span of `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary`, and the - `:143-147`-versus-`:143-146` span of the clear-on-rebind block — and states for each that no - acceptance criterion's binding clause is affected. `spec.md` itself is not edited for these; only - the acceptance-criteria checkboxes are edited by this phase. + `evidence/other/p8-t32-spec-tree-discrepancies.md`. Most of these arise because `spec.md` was + authored against the tree before issue #638 merged, which changed `EfcDataModel.cs` and shifted + every line citation into it. Acceptance: the artifact records each item below, stating for each + the figure `spec.md` carries, the figure measured on the merged tree, and whether any acceptance + criterion's binding clause is affected. + 1. `EfcDataModel.cs` line count: `spec.md` states 424; the merged tree is **485**. Corrected in + AC25 by this plan. Binding clause ("at or under 500 lines") unaffected, but the derived + headroom of 15 lines is what forced the change-B file split. The implementation-table row at + `spec.md:401` and the headroom sentence at `spec.md:414-416` retain the 424 figure and its + derived 76-line headroom; they are recorded here rather than corrected, because no acceptance + criterion depends on them and the file split they authorize is taken on the measured figure. + 2. `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary`: AC23 cites `:167-213`; the + span is `:167-214`. Not corrected in `spec.md`. Binding clause unaffected. + 3. The `#499` clear-on-rebind block: `spec.md` cites `BreadcrumbBridgeRouter.cs:143-146`; the + block spans `:143-147`. Not corrected in `spec.md`. The write at `:145` and the read at + `:143` that AC24 names are exact, so AC24's binding clause is unaffected. + 4. The `string` overload declaration of `MoveToFolderAsync`: AC16 cited `EfcDataModel.cs:259`; + it is at **303**. Corrected in AC16 by this plan. + 5. The trash-sentinel comparison: AC5 cites `EfcDataModel.cs:272`; it is at **316**. Not + corrected in `spec.md`. AC5's binding clause is behavioral and is unaffected. + 6. The `DestinationOlStem` assignment in the `string` overload: AC11 cited + `EfcDataModel.cs:287`; it is at **337**. Corrected in AC11 by this plan. + 7. `OpenOlFolderAsync`: AC17 cited `:299-316`; it spans **349-372**. Corrected in AC17 by this + plan as part of that criterion's rewording. + 8. `OpenFsFolderAsync`: AC17 cited `:318-334`; it spans **374-396**. Corrected in AC17 by this + plan. + 9. The `MAPIFolder` overload: AC15 and AC16 cited `:336-357` and `:336`; it spans **398-419** + with its declaration at **398**. Corrected in both criteria by this plan. + 10. The `ToArchiveRelativeStem` call inside the `MAPIFolder` overload: AC15 cited `:345`; it is + at **407**. Corrected in AC15 by this plan. + 11. The `MoveToFolderAsync` delegation call inside the `MAPIFolder` overload: AC16 cited + `:346`; it is at **408**, and the call spans **408-414**. Corrected in AC16 by this plan. + 12. `ToArchiveRelativeStem` itself: AC15 cited `:372-386`; the declaration is at **434** and the + documented member spans **421-448**. Corrected in AC15 by this plan. + 13. The `MoveToFolder` family census: AC16 stated 3 declarations and 6 call sites, and `spec.md` + elsewhere states 16 stem lines across 5 files. The merged tree carries **23** stem lines + across **6** files and **10** syntax-anchored lines across **5** files, classified as 3 + declarations and **7** call sites. Corrected in AC16 by this plan. + 14. AC17's original clause required that no `Globals.Ol.ArchiveRootPath` read gains a new + try/catch or degrade. Issue #638 had already given the file's single read both, at + `EfcDataModel.cs:284` inside the `try` at 282-286, with the degrade at 358 and 382, so the + clause was false on the merged tree before this plan ran and no action this plan authorizes + could make it true. AC17 was therefore reworded in `spec.md` to require that #638's guarded + read and degrade are preserved unchanged. + 15. `spec.md:164-172` describes the whole `Globals.Ol.ArchiveRootPath` benign-degrade item as an + open non-goal owned by issue #695, and cites the two verbatim `DestinationOlStem` + assignments as `:308` and `:326`. The `EfcDataModel` half of that item shipped in issue #638 + and is no longer pending; the two assignments are at **364** and **388** and do remain + verbatim, so that half of the statement still holds. Not corrected in `spec.md`; this is + prose outside the acceptance criteria and no binding clause depends on it. + `spec.md` is edited by this plan only where an acceptance criterion would otherwise be + unsatisfiable or would name the wrong file — items 1, 4, 6, 7, 8, 9, 10, 11, 12, 13 and 14, plus + the file-split renaming in AC11, AC15 and AC25. Items 2, 3, 5 and 15 are recorded here and not + corrected, because each is a citation whose acceptance criterion remains satisfiable as written. + The acceptance-criteria count in `spec.md` is unchanged at 30: no criterion is added, removed, or + split by any of these corrections. - [ ] [P8-T33] Final commit and clean tree. Run `git add docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` then @@ -1326,10 +1759,13 @@ is checked off before its cited evidence exists. Exactly one criterion is checke artifact and this plan file, with every other feature-folder path already in `HEAD`. Record both outputs verbatim. The pathspec scoping is required because `.claude/` is tracked and carries unrelated in-flight modifications that this plan must not commit, and because sibling - feature folders under `docs/features/active` are owned by other work. No sibling folder there is - untracked at planning time, but this task runs long after planning, and a concurrent run in this - checkout can leave an untracked or modified sibling folder under that parent directory before - this task executes; a `git add` over the parent directory would then commit another feature's + feature folders under `docs/features/active` are owned by other work. At least one sibling folder + there, + `docs/features/active/2026-08-07-breadcrumb-left-right-arrow-parent-child-navigation-440`, is + untracked in this checkout as recorded under "Git pathspec scoping", and this task also runs long + after planning, so a concurrent run in this + checkout can leave a further untracked or modified sibling folder under that parent directory + before this task executes; a `git add` over the parent directory would then commit another feature's work onto this branch, and a `git status --porcelain` span over the parent directory would report that folder and make this gate unsatisfiable. Both spans are therefore scoped so that this gate cannot depend on state this plan does not own. The feature-folder diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md index 82a3685b1..0595b3cfa 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md @@ -897,10 +897,12 @@ under "Corrections to the research file". ### Change B — normalization in the `string` overload of `MoveToFolderAsync` -- [ ] AC11. `QuickFiler/Controllers/EfcDataModel.cs` declares exactly one new `internal static` +- [ ] AC11. `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, a new partial-class file of + `EfcDataModel`, declares exactly one new `internal static` helper that takes the candidate path and the archive ancestor and returns the value to assign, and the `DestinationOlStem` assignment in the `string` overload (currently - `EfcDataModel.cs:287`) calls it. The helper is pure: no I/O, no logging, no static mutable + `EfcDataModel.cs:337`) calls it. The member is therefore `EfcDataModel.ToFilingStemOrVerbatim`, + unchanged by the file split. The helper is pure: no I/O, no logging, no static mutable state, and it is invoked directly by unit tests without constructing an `EmailFiler`. - [ ] AC12. The helper is gated on `ArchiveStemContract.IsFullOutlookPath`. Any value that is not a full Outlook path — every ordinary relative stem and the `"Trash to Delete"` sentinel — is @@ -913,21 +915,26 @@ under "Corrections to the research file". decides exactly as it does today. This is a deliberate divergence from `EfcDataModel.ToArchiveRelativeStem`, which throws on the archive-root-exact input; the rationale is recorded under "Error handling and logging updates". -- [ ] AC15. `EfcDataModel.ToArchiveRelativeStem` (`EfcDataModel.cs:372-386`), the `MAPIFolder` - overload (`:336-357`), and its call to `ToArchiveRelativeStem` at `:345` are unmodified, and +- [ ] AC15. `EfcDataModel.ToArchiveRelativeStem` (`EfcDataModel.cs:421-448`, declaration at `:434`), + the `MAPIFolder` + overload (`:398-419`), and its call to `ToArchiveRelativeStem` at `:407` are unmodified, and the **8** existing `ToArchiveRelativeStem` tests in `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` (methods at :21, :34, :48, :62, :72, :87, :100, :111) pass unchanged — including `ToArchiveRelativeStem_ArchiveRootItself_Throws`. - [ ] AC16. The `MoveToFolder` family is unchanged in shape apart from the helper call: still - **3** declarations (`EfcDataModel.cs:259`, `EfcDataModel.cs:336`, and the same-named forwarder - at EfcHomeController.ExecuteMoves.cs:89) and **6** call sites (EfcHomeController.ExecuteMoves.cs:78 - and :98, `EfcDataModel.cs:346`, EfcFormController.cs:537 and :844, and - QuickFiler.Test/Controllers/EfcHomeControllerExecuteMovesTests.cs:87). No new overload and no - signature change. -- [ ] AC17. `EfcDataModel.OpenOlFolderAsync` (`:299-316`) and `OpenFsFolderAsync` (`:318-334`) are - **not** modified, and no `Globals.Ol.ArchiveRootPath` read anywhere gains a new try/catch or - degrade — those are non-goals owned by issue #695. + **3** declarations (`EfcDataModel.cs:303`, `EfcDataModel.cs:398`, and the same-named forwarder + at EfcHomeController.ExecuteMoves.cs:89) and **7** call sites (EfcHomeController.ExecuteMoves.cs:78 + and :98, `EfcDataModel.cs:408`, EfcFormController.cs:537 and :844, + QuickFiler.Test/Controllers/EfcHomeControllerExecuteMovesTests.cs:87, and + QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs:314). The family-stem search over + `*.cs` returns **23** lines across **6** files, and the syntax-anchored search returns **10** + lines across **5** files. No new overload and no signature change. +- [ ] AC17. `EfcDataModel.OpenOlFolderAsync` (`:349-372`) and `OpenFsFolderAsync` (`:374-396`) are + **not** modified, and the guarded `Globals.Ol.ArchiveRootPath` read at `EfcDataModel.cs:284` + together with the `UserDiagnosticAction(ArchiveRootUnavailableMessage)` degrade at `:358` and + `:382`, both introduced by issue #638, are preserved unchanged. The remaining benign-degrade + work is a non-goal owned by issue #695. ### Change C — test spec correction @@ -967,9 +974,11 @@ under "Corrections to the research file". `BreadcrumbBridgeRouter.Selection.cs:138`, EfcFormController.cs:321). No new write site, no new public API member, and the property's `private set` is preserved. - [ ] AC25. File-size limits hold. `QuickFiler/Controllers/EfcDataModel.cs` remains at or under 500 - lines (424 before the change); + lines (485 before the change, leaving 15 lines of headroom, which is why the change-B helper is + declared in its own partial-class file as this document's implementation section authorizes); `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` does not grow beyond its current 694 lines; and every new or modified file, including + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` and `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, is at or under 500 lines. - [ ] AC26. `QuickFiler.Test/QuickFiler.Test.csproj` contains a `` item for From dd874dea582e78eca62746606a589ba6cf6c34d3 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sat, 29 Aug 2026 21:28:32 -0400 Subject: [PATCH 03/16] wip(637): close the three confirming-round preflight findings Add the porcelain companion to P0-T8 so the Phase 0 clean-baseline gate can observe a modified-but-unstaged tracked file, correct the misidentified stem-only search file to EfcHomeControllerTests.cs, and remove a sibling-folder tracking claim that is worktree-local and was measured in the wrong checkout. Refs #637 Co-Authored-By: Claude Sonnet 5 --- .../agent-memory/atomic-executor/MEMORY.md | 1 + ...nd_executor_observe_different_worktrees.md | 33 +++++++++ .claude/agent-memory/atomic-planner/MEMORY.md | 2 +- ...ct_637_selectrow_rooted_path_plan_seams.md | 12 +++- .../plan.2026-08-29T12-20.md | 72 ++++++++++--------- 5 files changed, 86 insertions(+), 34 deletions(-) create mode 100644 .claude/agent-memory/atomic-executor/project_planner_and_executor_observe_different_worktrees.md diff --git a/.claude/agent-memory/atomic-executor/MEMORY.md b/.claude/agent-memory/atomic-executor/MEMORY.md index 6a744b810..115950f85 100644 --- a/.claude/agent-memory/atomic-executor/MEMORY.md +++ b/.claude/agent-memory/atomic-executor/MEMORY.md @@ -4,6 +4,7 @@ - [Blocked Bash command drops chained check-off](project_blocked_bash_command_silently_drops_chained_checkoff.md) — aborts the WHOLE - [CSharpier chain-wrap defeats single-line search gates](project_csharpier_chain_wrap_defeats_singleline_search_gates.md) — zero-hit gate go - [Verify line citations with numbered output](feedback_verify_line_citations_with_numbered_output.md) — never hand-count +- [Planner and executor observe different worktrees](project_planner_and_executor_observe_different_worktrees.md) — `git status` claims don't travel - [Extract gate literals from the plan, never re-type](project_preflight_gate_literal_extract_from_plan_not_retype.md) — quoting drift - [Tool layer collapses `\` in file content](project_tool_layer_collapses_double_backslash_in_file_content.md) — heredocs and Wri - [Self-derived gate thresholds are blind](project_preflight_selfderived_gate_thresholds_are_blind.md) — floor from the r diff --git a/.claude/agent-memory/atomic-executor/project_planner_and_executor_observe_different_worktrees.md b/.claude/agent-memory/atomic-executor/project_planner_and_executor_observe_different_worktrees.md new file mode 100644 index 000000000..e3c50e93b --- /dev/null +++ b/.claude/agent-memory/atomic-executor/project_planner_and_executor_observe_different_worktrees.md @@ -0,0 +1,33 @@ +--- +name: planner-and-executor-observe-different-worktrees +description: A plan's claim about worktree-local git state (untracked siblings, dirty files, clean status) can be true in the planner's worktree and false in the executor's; always re-derive it in the executor's checkout +metadata: + type: project +--- + +A plan assertion about **worktree-local** git state — "sibling folder X is untracked in this +checkout", "the tree is dirty", "path Y is not in the index" — is worktree-scoped, and the planner +and the executor frequently run in different worktrees. `atomic-planner` often runs in the session +worktree (`TaskMaster-wt/`), while `atomic-executor` runs in an isolated agent worktree +(`.claude/worktrees/agent-`) on a different branch. The same path can be untracked in one and +committed in the other. + +**Why:** on #637 round 2 the planner "corrected" a round-1 statement that no sibling folder under +`docs/features/active` was untracked, replacing it with an explicit claim that the `...-440` folder +"exists on disk ... and is reported as untracked by `git status --porcelain`". That was true in the +session worktree and false in the executor's: there `git ls-files` lists the 440 folder's files and +`git status --porcelain` is completely empty. A correct claim was reversed into a false one, and the +false claim was then propagated into a task body (`P8-T33`). This is the +[[project_preflight_citation_match_propagates_false_fact]] failure mode with a worktree twist: the +planner's observation was real, just about the wrong tree. + +**How to apply:** distinguish two claim classes when reviewing. +- *Commit-scoped* claims (`git diff BASE..HEAD`, `git ls-files`, file contents at a ref) are branch + properties and travel with the branch — verify once. +- *Worktree-scoped* claims (`git status --porcelain`, untracked-file existence, on-disk-but-not-in-index) + do NOT travel — re-derive them in the executor's cwd before accepting or contradicting them. + +Prefer plan prose that justifies pathspec scoping **prospectively** ("a concurrent run can leave an +untracked sibling before this task executes") over prose that asserts a specific present-tense tree +state, because the prospective form is worktree-neutral and cannot go stale between planning and +execution. diff --git a/.claude/agent-memory/atomic-planner/MEMORY.md b/.claude/agent-memory/atomic-planner/MEMORY.md index 96d3a8916..9d7370435 100644 --- a/.claude/agent-memory/atomic-planner/MEMORY.md +++ b/.claude/agent-memory/atomic-planner/MEMORY.md @@ -19,7 +19,7 @@ - [#677 R1–R8](project_677_keyboard_focus_leak_plan_seams.md) — ctor param REJECTED (5 reflection-arity tests); typed harness for compile-red; internal 9-arg ctor, never ambient SetSynchronizationContext; per-file non-vacuity floors - [#635](project_635_reflective_caller_audit_plan_seams.md) — evidence-only audit: tracked plan inflates its own sweep; scan hits its own pattern list; spec said six sites, tree has eight; pathspec breadth inflates a count - [#440 R1–R4](project_440_breadcrumb_left_arrow_plan_seams.md) — deletion-only change voids a diff-derived changed-line gate; `(Rebuild target(s))`, NOT `(Rebuild target)`; `.csharpierignore` matches the `.cobertura.xml` suffix; `.dotnet-sdk` IS gitignored (`.gitignore:350`); cite an AC by sentence only after counting its sentences; `Include` resolves against the declaring project's dir -- [#637 R2/R3](project_637_selectrow_rooted_path_plan_seams.md) — `docs/features/active` operand hits 121 sibling evidence files; the untracked-440 claim was FALSE for the agent worktree; blanket `-F` breaks every regex; pre-format range consumed post-format; probe with no branch for the outcome that occurs +- [#637 R2/R3/R4](project_637_selectrow_rooted_path_plan_seams.md) — `docs/features/active` operand hits 121 sibling evidence files; the untracked-440 claim was FALSE for the agent worktree; blanket `-F` breaks every regex; pre-format range consumed post-format; probe with no branch for the outcome that occurs; set-difference file named by recency - [#680](project_680_menu_mode_plan_seams.md) — HostTests.cs 499 not 500; set-difference format gate; TRX 5-shape identifiers, `grep -a`; append-a-dated-literal discriminator; post-merge remediation: exact line arithmetic — the review's "optional" fallback was load-bearing (501 vs 500) ## Plan-structure traps diff --git a/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md b/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md index f33d3f088..338859007 100644 --- a/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md +++ b/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md @@ -1,6 +1,6 @@ --- name: project-637-selectrow-rooted-path-plan-seams -description: Issue #637 breadcrumb SelectRow plan — preflight R2/R3 seams: docs/features/active as a command operand reaches 121 sibling evidence files; the untracked-440 claim was false for the agent worktree; a blanket -F rule breaks every regex; pre-format line ranges consumed post-format. +description: Issue #637 breadcrumb SelectRow plan — preflight R2/R3/R4 seams: docs/features/active as a command operand reaches 121 sibling evidence files; the untracked-440 claim was false for the agent worktree; a blanket -F rule breaks every regex; pre-format line ranges consumed post-format; a set-difference file named by recency instead of by the difference. metadata: type: project --- @@ -49,6 +49,16 @@ XML documentation phrasing AC12's contract (`IsFullOutlookPath` again), and a do against every authoring instruction in the same *and* later tasks over the same file. See also [[single-numeral-gates-must-name-the-role]] and [[absolute-counts-in-shared-files-go-stale]]. +**R4: name a set-difference file by the difference, not by recency.** The plan called +`EfcDataModelArchiveRootTests.cs` "the sixth file in the stem search" because issue #638 had just added +it. It is in **both** search sets — `rg -n "MoveToFolderAsync\s*\(" .../EfcDataModelArchiveRootTests.cs` +returns `:314`, the call site the plan itself enumerates. The file reached by the stem search only is +`QuickFiler.Test/Controllers/EfcHomeControllerTests.cs`, whose single `MoveToFolder` hit at `:55` is a +comment. An executor running both searches would have seen the named file in both sets and had to write +a falsehood or halt. **How to apply:** when a plan asserts "the file in A but not B", actually compute +A minus B; do not infer it from which file changed most recently. The census totals (23/6 and 10/5) were +correct — only the identification was wrong, so a totals check would not have caught it. + **A probe must state what each outcome means.** An `ON_PATH`/`NOT_ON_PATH` `msbuild` probe was added to justify a vswhere substitution, but the plan supplied no branch for `ON_PATH` — which is the outcome that actually occurs in this worktree — so the artifact would have recorded an observation diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index 4cef22822..2ac13b4b7 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -3,9 +3,9 @@ - **Issue:** #637 - **Parent (optional):** none - **Owner:** drmoisan -- **Last Updated:** 2026-08-29T14-05 +- **Last Updated:** 2026-08-29T15-10 - **Status:** Draft -- **Version:** 0.3 +- **Version:** 0.4 - **Work Mode:** full-bug (from `issue.md`); `spec.md` is the sole acceptance-criteria source (AC1-AC30). ## Conventions (read before executing any task) @@ -42,20 +42,15 @@ in-flight modifications, and `docs/features/parallel/` and `artifacts/` are owne Every `git status --porcelain` and `git diff` gate in this plan is therefore scoped with an explicit pathspec naming only first-party source, test and feature-document trees. The feature-document component of every such pathspec is this feature's own folder and never the parent directory -`docs/features/active`. That narrowing is load-bearing rather than cosmetic, and it is required by the -tree as it stands now as well as by the tree the executor will meet. A sibling folder is untracked in -this checkout at the time of this revision: -`docs/features/active/2026-08-07-breadcrumb-left-right-arrow-parent-child-navigation-440` exists on -disk, carrying `issue.md`, `plan.2026-08-29T00-22.md` and `spec.md`, and is reported as untracked by -`git status --porcelain`. An earlier revision of this plan asserted the opposite — that no sibling -folder was untracked and that the 440 folder's paths were present in this worktree's git index — and -that assertion was wrong for this worktree; it is corrected here rather than carried forward, because -a false premise under a correct conclusion is a defect a later reader would have to rediscover. A -`git add` over the parent directory would therefore stage that other feature's folder onto this branch -today, not merely under some future condition. The narrowing is also required prospectively, because -the executor runs later than this planning pass: this repository carries several concurrent worktrees and -in-flight feature folders, and a concurrent run in this checkout can leave an untracked or modified -sibling folder under `docs/features/active` at any point between planning and execution. A `git add` +`docs/features/active`. That narrowing is load-bearing rather than cosmetic, and it is required +prospectively rather than by any present-tense observation of the tree. Whether a given sibling +folder under `docs/features/active` is tracked, untracked, or modified is a worktree-local property +that does not travel with the branch: this repository carries several concurrent worktrees on +different branches, and the checkout in which this plan was authored is not necessarily the one in +which it is executed. This plan therefore records no claim about the present tracking state of any +sibling folder and does not depend on one. The executor also runs later than this planning pass, and a +concurrent run in this checkout can leave an untracked or modified sibling folder under +`docs/features/active` at any point between planning and execution. A `git add` over the parent directory would then stage and commit another feature's folder onto this branch, and a `git status --porcelain` over the parent directory would report that folder and make every emptiness gate that consumes it unsatisfiable. This plan does not assume that the tree it observed at planning @@ -253,8 +248,9 @@ plan creates and leaves uncommitted. Every name-listing diff in this plan theref `git add` span or a `git status --porcelain` span in the same task, and the task text states what the executor must observe in that companion output. The two mechanisms are complementary and each alone is wrong in one state: the anchored diff is blind to untracked files, and porcelain status goes empty -once the change is committed. This plan contains exactly two name-listing diff sites, and both carry a -companion. P6-T6 runs +once the change is committed. This plan contains exactly three name-listing diff sites, and all three +carry a companion. P0-T8 runs `git status --porcelain -- QuickFiler QuickFiler.Test` alongside its +baseline diff and asserts that span is empty. P6-T6 runs `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` before its diff and asserts that `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output after the commit. P8-T30 runs a porcelain span over the nine trees it audits, ahead of its two diffs, and @@ -395,8 +391,11 @@ no downstream artifact inherits a wrong figure. across **5** files: 3 declarations and 7 call sites, leaving 13 residual non-member textual references. Research section 6 says 16 lines across 6 files and `spec.md` says 16 across 5; both describe the pre-merge tree. AC16's counts and citations are corrected in `spec.md`; P1-T2 and - P8-T16 carry the measured figures. The sixth file in the stem search is - `QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs`, added by issue #638. + P8-T16 carry the measured figures. The one file the stem search reaches that the syntax-anchored + search does not is `QuickFiler.Test/Controllers/EfcHomeControllerTests.cs`, whose single match at + `:55` is a comment naming `MoveToFolderAsync` and not a member reference. + `QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs`, added by issue #638, appears in both + searches and contributes the call site at `:314`. 6. This worktree has no `.dotnet-sdk` directory and no `packages` directory, so the repo-local SDK and the NuGet package restore must both be bootstrapped before any toolchain command runs. 7. Issue #638 landed on this branch before execution begins. It changed @@ -447,10 +446,17 @@ no downstream artifact inherits a wrong figure. `MoveToFolder` five-file correction, and the `SelectedFolderPath` three-production-file correction. - [ ] [P0-T8] Record the branch and prove the base commit is a clean pre-change baseline. Run - `git rev-parse --abbrev-ref HEAD`, `git rev-parse HEAD`, and + `git rev-parse --abbrev-ref HEAD`, `git rev-parse HEAD`, `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler QuickFiler.Test`, - and write `evidence/baseline/p0-t8-git-base.md` recording all three outputs verbatim. - Acceptance: the `git diff --name-only` invocation produces **no output at all**, and the + and, as the porcelain companion to that name-listing diff, + `git status --porcelain -- QuickFiler QuickFiler.Test`, + and write `evidence/baseline/p0-t8-git-base.md` recording all four outputs verbatim. The porcelain + span is required because a name-listing diff compares two commits and therefore reports neither an + untracked path nor a modified-but-unstaged tracked file; either state falsifies the clean-baseline + conclusion this task exists to establish, and both would otherwise surface for the first time at + P6-T6's exact ten-path enumeration. + Acceptance: the `git diff --name-only` invocation produces **no output at all**; the + `git status --porcelain` invocation produces **no output at all**; and the recorded branch name is `bug/breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. An ancestry check is deliberately not used here. `git merge-base --is-ancestor` exits 0 for any ancestor, including @@ -458,7 +464,8 @@ no downstream artifact inherits a wrong figure. exists to detect; the empty-diff form fails as soon as any file under `QuickFiler` or `QuickFiler.Test` differs between the anchor and `HEAD`, which is the property every later "exactly N paths", "no hunk in range" and "added line numbers" gate depends on. If the branch - name differs, or if the diff produces any output, record `BASE MISMATCH` in the artifact + name differs, if the diff produces any output, or if the porcelain span produces any output, + record `BASE MISMATCH` in the artifact together with the offending output, stop, and report to the orchestrator; do not proceed to P0-T9. - [ ] [P0-T9] Bootstrap the repo-local .NET SDK with @@ -593,8 +600,11 @@ second, independently constructed search. No number in this phase is verified by `EfcDataModelArchiveRootTests.cs:314`); and the artifact records that Search 1 minus Search 2 leaves exactly 13 non-member textual references, closing the 23-line accounting. The artifact also records that the stem-search file count is 6 and the syntax-anchored file count is - 5, that the sixth stem-search file is `EfcDataModelArchiveRootTests.cs`, and that the 16-line - figure in research section 6 and in `spec.md` describes the tree before issue #638 merged. + 5, that the one stem-search file the syntax-anchored search does not reach is + `EfcHomeControllerTests.cs`, whose single match at `:55` is a comment, and that + `EfcDataModelArchiveRootTests.cs` appears in both searches, contributing the `:314` call site, + and that the 16-line figure in research section 6 and in `spec.md` describes the tree before + issue #638 merged. - [ ] [P1-T3] Re-derive the `SelectedFolderPath` surface (AC24: 9 lines across 3 production files, 2 writes, 3 reads). Search 1: `rg -c "SelectedFolderPath" --glob "*.cs" .`, recording the per-file counts. Search 2, independently constructed by scoping to the production project directories up @@ -1759,12 +1769,10 @@ is checked off before its cited evidence exists. Exactly one criterion is checke artifact and this plan file, with every other feature-folder path already in `HEAD`. Record both outputs verbatim. The pathspec scoping is required because `.claude/` is tracked and carries unrelated in-flight modifications that this plan must not commit, and because sibling - feature folders under `docs/features/active` are owned by other work. At least one sibling folder - there, - `docs/features/active/2026-08-07-breadcrumb-left-right-arrow-parent-child-navigation-440`, is - untracked in this checkout as recorded under "Git pathspec scoping", and this task also runs long - after planning, so a concurrent run in this - checkout can leave a further untracked or modified sibling folder under that parent directory + feature folders under `docs/features/active` are owned by other work. This task runs long after + planning, and the tracking state of a sibling folder under that parent directory is + worktree-local and unobserved by this plan, so a concurrent run in this + checkout can leave an untracked or modified sibling folder under that parent directory before this task executes; a `git add` over the parent directory would then commit another feature's work onto this branch, and a `git status --porcelain` span over the parent directory would report that folder and make this gate unsatisfiable. Both spans are therefore scoped so that this gate From 637c1d6891606d128140f3de7c4f7349112c0822 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sat, 29 Aug 2026 21:35:52 -0400 Subject: [PATCH 04/16] docs(637): record the stale-base-anchor trap in orchestrator memory Refs #637 Co-Authored-By: Claude Sonnet 5 --- .claude/agent-memory/orchestrator/MEMORY.md | 1 + .../stale-base-anchor-passes-ancestry-vacuously.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 .claude/agent-memory/orchestrator/stale-base-anchor-passes-ancestry-vacuously.md diff --git a/.claude/agent-memory/orchestrator/MEMORY.md b/.claude/agent-memory/orchestrator/MEMORY.md index 6354bb177..e114702f5 100644 --- a/.claude/agent-memory/orchestrator/MEMORY.md +++ b/.claude/agent-memory/orchestrator/MEMORY.md @@ -110,6 +110,7 @@ - [Resumed child orchestrator shares your worktree](resumed-child-orchestrator-shares-worktree.md) — a checkpoint mtime you did not author - [Shared checkpoint: never read-modify-write](shared-checkpoint-read-modify-write-corrupts.md) - a sibling swaps the session-root file - [External actor can merge your child PR mid-run](external-actor-can-merge-your-child-pr-midrun.md) — re-read PR state before the CI gate +- [Stale base anchor passes ancestry vacuously](stale-base-anchor-passes-ancestry-vacuously.md) — on a prep resume the pinned base stays an ancestor, so the check passes while diffs bill another issue's work to your plan ## Artifact hygiene - [Angle-bracket redaction breaks TRX XML](angle-bracket-redaction-breaks-trx-xml.md) — a `` in an XML attribute makes the diff --git a/.claude/agent-memory/orchestrator/stale-base-anchor-passes-ancestry-vacuously.md b/.claude/agent-memory/orchestrator/stale-base-anchor-passes-ancestry-vacuously.md new file mode 100644 index 000000000..b670b76ff --- /dev/null +++ b/.claude/agent-memory/orchestrator/stale-base-anchor-passes-ancestry-vacuously.md @@ -0,0 +1,14 @@ +--- +name: stale-base-anchor-passes-ancestry-vacuously +description: On a preparation resume, a plan's pinned base commit stays an ancestor of HEAD after main advances, so the plan's own merge-base check passes while every diff gate silently attributes another issue's work to this plan +metadata: + type: project +--- + +When resuming an interrupted preparation run, re-fetch `origin/main` and diff it against the plan's pinned base anchor before doing anything else. If main advanced, merge it into the item branch and re-anchor every `git diff` in the plan to the merge commit. + +**Why:** A plan pins a base SHA and asserts `git merge-base --is-ancestor HEAD` exits 0. After main advances and is merged in, that assertion *still passes* — the old commit is still an ancestor — so the plan's own self-check reports a clean baseline while `git diff ..HEAD` already lists the files the intervening PR changed. Every downstream "exactly N files changed", "no hunk in this range", and changed-line-coverage gate then measures the other issue's work as if this plan produced it. Ancestry is not currency. + +Concrete instance (issue #637, 2026-08-29): base `ecdb1c84`, main advanced to `fa2ddefa` via PR #700 (issue #638). Ancestry check passed; three files were already changed against the anchor. `EfcDataModel.cs` had gone 423 to 485 lines, moving every cited line number, and the plan still instructed writing `Globals.Ol.ArchiveRootPath` at a site where #638 had just replaced that unguarded read with a guarded `TryGetArchiveRoot(out var olAncestor)` local. Following the plan verbatim would have regressed #638 and failed its merged regression test, and the 62 added lines had cut 500-line headroom from 77 to 15, making the plan's own file-size gate unsatisfiable. + +**How to apply:** Replace the vacuous ancestry assertion with one that can fail — assert `git diff --name-only ..HEAD -- ` produces no output — and pair it with a `git status --porcelain` companion, since a commit-to-commit diff is blind to both untracked files and modified-but-unstaged tracked ones. See [[prepared-epic-child-invalidated-by-sibling-merge]] for the related case where a sibling merge invalidates a prepared child outright. From 5b04bec14a90180b64dd0af07797d154590f2089 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sun, 30 Aug 2026 07:39:57 -0400 Subject: [PATCH 05/16] docs(637): re-anchor the atomic plan on the post-merge baseline origin/main 69aa28dd was merged into this branch at 0eda184c. The plan's diff anchor b9476588 was no longer a clean pre-change baseline: six files under the plan's own QuickFiler and QuickFiler.Test pathspec differ between that anchor and HEAD for reasons this plan does not own, so every anchored footprint-containment gate was unsatisfiable. All 19 anchored sites now name 0eda184c. Adds tree observation 9 recording the merge, the new Compile Include at QuickFiler.Test.csproj:133, and the re-derived citations that did not move. No scope, phase, identifier, or acceptance-criteria change. Refs #637 Co-Authored-By: Claude Opus 5 --- .claude/agent-memory/atomic-planner/MEMORY.md | 2 +- ...ct_637_selectrow_rooted_path_plan_seams.md | 27 +++++- .../plan.2026-08-29T12-20.md | 92 +++++++++++++------ 3 files changed, 90 insertions(+), 31 deletions(-) diff --git a/.claude/agent-memory/atomic-planner/MEMORY.md b/.claude/agent-memory/atomic-planner/MEMORY.md index 3a2c9be0b..66eeee394 100644 --- a/.claude/agent-memory/atomic-planner/MEMORY.md +++ b/.claude/agent-memory/atomic-planner/MEMORY.md @@ -19,7 +19,7 @@ - [#677 R1–R8](project_677_keyboard_focus_leak_plan_seams.md) — ctor param REJECTED (5 reflection-arity tests); typed harness for compile-red; internal 9-arg ctor, never ambient SetSynchronizationContext; per-file non-vacuity floors - [#635](project_635_reflective_caller_audit_plan_seams.md) — evidence-only audit: tracked plan inflates its own sweep; scan hits its own pattern list; spec said six sites, tree has eight; pathspec breadth inflates a count - [#440 R1–R4](project_440_breadcrumb_left_arrow_plan_seams.md) — deletion-only change voids a diff-derived changed-line gate; `(Rebuild target(s))`, NOT `(Rebuild target)`; `.csharpierignore` matches the `.cobertura.xml` suffix; `.dotnet-sdk` IS gitignored (`.gitignore:350`); cite an AC by sentence only after counting its sentences; `Include` resolves against the declaring project's dir -- [#637 R2/R3/R4](project_637_selectrow_rooted_path_plan_seams.md) — `docs/features/active` operand hits 121 sibling evidence files; the untracked-440 claim was FALSE for the agent worktree; blanket `-F` breaks every regex; pre-format range consumed post-format; probe with no branch for the outcome that occurs; set-difference file named by recency +- [#637 R2–R5](project_637_selectrow_rooted_path_plan_seams.md) — `docs/features/active` operand hits 121 sibling evidence files; the untracked-440 claim was FALSE for the agent worktree; blanket `-F` breaks every regex; pre-format range consumed post-format; probe with no branch for the outcome that occurs; set-difference file named by recency; R5 re-anchor on the merge commit, `#NNN` at column 0, CRLF round-trip check - [#644](project_644_ac16_referral_revision_seams.md) — AC's named instrument prints no figure; substitute's noise (~0.028pp) exceeds the 0.01pp delta; check-off task → referral task pinning the UNCHECKED state - [#644 cycle 2](project_644_cycle2_sweep_gate_evasion_seams.md) — rewording out of a detector's match set is gate evasion; SHA-256 pair for untracked prior artifacts; bound the porcelain `??` scope - [#644 PA-7](project_644_pa7_redaction_plan_seams.md) — untracked audit artifact still enters main; name-status diff blind to it; mandated literal contradicted its own stated intent diff --git a/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md b/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md index 338859007..ad8c02380 100644 --- a/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md +++ b/.claude/agent-memory/atomic-planner/project_637_selectrow_rooted_path_plan_seams.md @@ -1,6 +1,6 @@ --- name: project-637-selectrow-rooted-path-plan-seams -description: Issue #637 breadcrumb SelectRow plan — preflight R2/R3/R4 seams: docs/features/active as a command operand reaches 121 sibling evidence files; the untracked-440 claim was false for the agent worktree; a blanket -F rule breaks every regex; pre-format line ranges consumed post-format; a set-difference file named by recency instead of by the difference. +description: Issue #637 breadcrumb SelectRow plan — preflight R2/R3/R4 seams plus the R5 base-anchor reconciliation: docs/features/active as a command operand reaches 121 sibling evidence files; the untracked-440 claim was false for the agent worktree; a blanket -F rule breaks every regex; pre-format line ranges consumed post-format; a set-difference file named by recency; re-anchoring after a mid-plan origin/main merge; rewrapped prose putting #NNN at column 0. metadata: type: project --- @@ -59,6 +59,31 @@ a falsehood or halt. **How to apply:** when a plan asserts "the file in A but no A minus B; do not infer it from which file changed most recently. The census totals (23/6 and 10/5) were correct — only the identification was wrong, so a totals check would not have caught it. +**R5: a mid-plan `origin/main` merge invalidates every anchored footprint gate.** The plan carried +its anchor at 19 distinct lines. When the orchestrator merged the current `origin/main` into the +branch, the previous anchor stayed an ancestor of `HEAD` but stopped being clean: six unowned files +now sat between it and `HEAD`, which makes P0-T8's empty-diff proof, the exact-N-path enumerations +and any tree-wide `git diff --cached -- ` gate unsatisfiable. Fix: re-anchor on the +**merge commit itself**, so everything the merge brought in is behind the anchor. +**How to apply:** grep the SHA and count the sites before editing — the delegating prompt's own site +list was short by one (a continuation line where the SHA starts at column 0 of the next line). +Re-anchoring on the merge commit means the anchor equals the branch tip; that is not a HEAD pin +provided no task asserts `git rev-parse HEAD` equals a value, and the empty-diff gate still fails on +any further commit while the porcelain companion still fails on staged/unstaged/untracked change. +Name only the gates the stale anchor actually breaks: per-file hunk-range gates scoped to files the +merge did not touch are unaffected, so listing them as "would be unsatisfiable" is a false claim. +See [[never-pin-head-sha-as-plan-expectation]] and [[diff-gates-need-a-commit-task]]. + +**Rewrapped prose can push `#NNN` to column 0 and look like an ATX heading.** Rewriting the base-commit +block produced a line beginning `#638) and that work was merged...`. CommonMark does not treat it as a +heading, but the plan validator's heading scan is line-oriented, and the plan contract requires the +exact `### Phase N — ` form. Rewrap so no line starts with `#`. Same failure family as +[[plan-fenced-powershell-comments-look-like-headings]]. + +**Editing a 100%-CRLF plan file: multi-line Edit round-trips CRLF, including inserted lines.** Measured +before and after with `rg -c '\r$'` versus `rg -c '^'` (equal counts = 100% CRLF). Verify with that +pair rather than assuming; a whole-file ending flip turns a 20-line amendment into a full-file diff. + **A probe must state what each outcome means.** An `ON_PATH`/`NOT_ON_PATH` `msbuild` probe was added to justify a vswhere substitution, but the plan supplied no branch for `ON_PATH` — which is the outcome that actually occurs in this worktree — so the artifact would have recorded an observation diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index 2ac13b4b7..e198a1282 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -3,9 +3,9 @@ - **Issue:** #637 - **Parent (optional):** none - **Owner:** drmoisan -- **Last Updated:** 2026-08-29T15-10 +- **Last Updated:** 2026-08-30T07-30 - **Status:** Draft -- **Version:** 0.4 +- **Version:** 0.5 - **Work Mode:** full-bug (from `issue.md`); `spec.md` is the sole acceptance-criteria source (AC1-AC30). ## Conventions (read before executing any task) @@ -29,13 +29,29 @@ path in prose is the one at `research/research.2026-08-29T12-30.md:6`, `<repo-root>/.claude/worktrees/<worktree-id>`. **Base commit** — the diff anchor for this plan is the literal commit -`b9476588e0e49e113c73cc55cc918f4a65e022fd`. Every `git diff` in this plan supplies it explicitly. No -task pins a HEAD SHA. This anchor supersedes the earlier anchor `ecdb1c84`, which this plan was first -authored against. `origin/main` advanced to `fa2ddefa` (pull request #700, issue #638) and that work -was merged into this branch, so `ecdb1c84` is still an ancestor of `HEAD` but is no longer a clean -pre-change baseline: three files differ between it and `HEAD` for reasons this plan does not own. -`b9476588e0e49e113c73cc55cc918f4a65e022fd` is the post-merge, pre-change baseline, and P0-T8 proves -that property by a check that can fail rather than by an ancestry check that cannot. +`0eda184ca0009bc79ac9b7146897270c17c095fa`. Every `git diff` in this plan supplies it explicitly. No +task pins a HEAD SHA: no acceptance condition in this plan asserts that `HEAD` equals a stated +value, and P0-T8 records `git rev-parse HEAD` as an observation rather than as a gate. This anchor +is the third this plan has carried, and both supersessions have the same cause. The plan was first +authored against `ecdb1c84`. `origin/main` then advanced to `fa2ddefa` (pull request #700, +issue #638) and that work was merged into this branch, which left `ecdb1c84` an ancestor of `HEAD` +but no longer a clean pre-change baseline, because files under `QuickFiler` and `QuickFiler.Test` +that this plan does not own then differed between it and `HEAD`; the anchor moved to `b9476588`. +`origin/main` has since advanced to `69aa28dd` (pull request #702, issue #644), and that work was +merged into this branch at `0eda184ca0009bc79ac9b7146897270c17c095fa`. That merge put `b9476588` +in exactly the position `ecdb1c84` had been in: the issue #644 navigation key-ledger work the merge +brought in lies between `b9476588` and `HEAD`, so a diff anchored there reports files this plan does +not own, and the gates that enumerate or scope over a whole tree — P0-T8's empty-diff baseline +proof, P6-T6's exact ten-path enumeration, P8-T30's `QuickFiler QuickFiler.Test` diff, and P5-T5's +tree-wide `QuickFiler.Test` diff — would be unsatisfiable as written. Anchoring at the merge commit +puts all of that work behind the anchor. +`0eda184ca0009bc79ac9b7146897270c17c095fa` is the post-merge, pre-change baseline, and P0-T8 proves +that property by a check that can fail rather than by an ancestry check that cannot. The proof is +not vacuous merely because this anchor is the branch tip at the moment this plan was amended. +P0-T8's diff fails as soon as any file under `QuickFiler` or `QuickFiler.Test` differs between the +anchor and `HEAD` in the checkout the executor actually runs in, and its porcelain companion fails +on any staged, unstaged or untracked change in those same trees. The anchor stops being the tip +at P6-T6, after which every later anchored diff carries this plan's own changes. **Git pathspec scoping** — `.claude/` is a tracked directory in this repository and carries unrelated in-flight modifications, and `docs/features/parallel/` and `artifacts/` are owned by other processes. @@ -237,9 +253,9 @@ file are outside the formatter's scope. **Anchored-diff form.** Before P6-T6 commits, nothing this plan changes is in `HEAD`, so a two-dot `BASE..HEAD` diff reports nothing for it. Every pre-commit diff gate in this plan therefore uses the -index form `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- <paths>` and is preceded in +index form `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- <paths>` and is preceded in the same task by a `git add` over the same paths. Every post-commit diff gate uses -`git diff b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- <paths>`. Both forms are anchored to an +`git diff 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- <paths>`. Both forms are anchored to an explicit ref; the bare unanchored `git diff` is never used. **Name-listing diffs carry a companion.** A `git diff --name-only` or `--name-status` enumerates @@ -415,6 +431,18 @@ no downstream artifact inherits a wrong figure. items. The `ItemGroup` carrying the `Controllers\` entries opens at `:287`, and `<Compile Include="Controllers\EfcDataModel.cs" />` is at `:289`. A production file absent from this project does not compile into the assembly. +9. Issue #644 (pull request #702) landed on this branch in the same merge that produced the current + base anchor, so all of that work is behind the anchor and appears in no diff this plan takes. It + added `QuickFiler.Test/Controllers/QfcCollectionControllerNavigationLedgerTests.cs` and registered + it at `QuickFiler.Test/QuickFiler.Test.csproj:133`, which is below every line this plan cites in + that file — `:57`, `:64`, `:114` and `:116` — so no cited line number in it shifted. The + citations most exposed to that merge were re-derived against the merged tree and are unchanged: + `QuickFiler/Controllers/EfcDataModel.cs` is still 485 lines with its declaration at `:21` and the + `DestinationOlStem` assignment at `:337`; `QuickFiler/QuickFiler.csproj` still carries 130 + `<Compile Include>` items with the `ItemGroup` at `:287` and `Controllers\EfcDataModel.cs` at + `:289`; and `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` is still 209 lines with + the discarded `out _` at `:99`. The `MoveToFolder` and `SelectedFolderPath` censuses in + observation 5 and in P1-T2 and P1-T3 were re-measured on the merged tree and are also unchanged. ### Phase 0 — Context, policy reads, and baseline capture @@ -447,14 +475,20 @@ no downstream artifact inherits a wrong figure. correction. - [ ] [P0-T8] Record the branch and prove the base commit is a clean pre-change baseline. Run `git rev-parse --abbrev-ref HEAD`, `git rev-parse HEAD`, - `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler QuickFiler.Test`, + `git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler QuickFiler.Test`, and, as the porcelain companion to that name-listing diff, `git status --porcelain -- QuickFiler QuickFiler.Test`, and write `evidence/baseline/p0-t8-git-base.md` recording all four outputs verbatim. The porcelain span is required because a name-listing diff compares two commits and therefore reports neither an untracked path nor a modified-but-unstaged tracked file; either state falsifies the clean-baseline conclusion this task exists to establish, and both would otherwise surface for the first time at - P6-T6's exact ten-path enumeration. + P6-T6's exact ten-path enumeration. `git rev-parse HEAD` is recorded as an observation and is + never compared against a stated value. The anchor is the commit at which `origin/main` was + merged into this branch, and it was the branch tip when this plan was amended, so on an + untouched checkout the name-listing diff is empty for that reason; it becomes non-empty the + moment the executor's checkout carries any further commit under those two trees, and the + porcelain companion beside it reports any staged, unstaged or untracked change in the same + trees. Together those are exactly the states this task exists to detect. Acceptance: the `git diff --name-only` invocation produces **no output at all**; the `git status --porcelain` invocation produces **no output at all**; and the recorded branch name is @@ -831,7 +865,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe `rg -n "class EfcDataModelIssue637Tests" QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` returns exactly 1 line; each of the eight fixed method names is found exactly once in that file; `git add QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` followed in the same task by - `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` + `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` shows zero removed content lines, meaning zero lines beginning with a single `-`; and `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs").Count` is at most 500. @@ -937,7 +971,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe pass-through mode is untouched. - [ ] [P3-T3] Verify AC8: `SelectHierarchyPath` and `CommitSelection` are unmodified. Run `git add QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` then - `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` + `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` and write `evidence/regression-testing/p3-t3-selectionfile-diff.md`. Acceptance: every hunk header in the diff addresses a line range that lies entirely within the original lines 83 to 107; no hunk touches the original line range 109 to 139; and the artifact lists the hunk headers @@ -1033,11 +1067,11 @@ redirect the assignment. Reversing any of those orders produces a build that doe `EXIT_CODE: 0`; the run reports 8 tests for that class; 8 passed including `ToArchiveRelativeStem_ArchiveRootItself_Throws`; and, after `git add QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, - `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` + `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` shows zero removed content lines. - [ ] [P4-T6] Verify AC17: the non-goals are untouched. Run `git add QuickFiler/Controllers/EfcDataModel.cs` then - `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -U0 -- QuickFiler/Controllers/EfcDataModel.cs` + `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -U0 -- QuickFiler/Controllers/EfcDataModel.cs` and write `evidence/regression-testing/p4-t6-nongoals-untouched.md`, quoting every hunk header verbatim. This plan makes exactly two edits to this file, both single-line substitutions, so the file's line count and every line number in it are unchanged and the diff is exactly two hunks. @@ -1107,12 +1141,12 @@ redirect the assignment. Reversing any of those orders produces a build that doe `evidence/regression-testing/p5-t4-provider-assertion-preserved.md`. Acceptance: lines 161 to 164 of `BreadcrumbBridgeRouterIssue439Tests.cs` are byte-identical to their pre-change text, quoted in the artifact; and, after `git add QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`, - `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` + `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` produces no output at all, since this plan changes no line of that file. - [ ] [P5-T5] Verify the file did not grow and that exactly one assertion changed. Run `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs").Count'`, then `git add QuickFiler.Test` followed by - `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- QuickFiler.Test`, and write + `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- QuickFiler.Test`, and write `evidence/regression-testing/p5-t5-single-assertion-change.md`. Acceptance: the line count is exactly 694; among the diff's removed content lines, exactly one matches `.Should()`, and it is ` router.SelectedFolderPath.Should().Be(fullTarget);`; and the artifact records the @@ -1169,7 +1203,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe `EXIT_CODE: 0` with 0 failed; and the artifact records that `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` passed and that neither `IsValidFilingSelection` nor `IsValidCreationSelection` had any executable line changed, verified - by a `git diff b9476588e0e49e113c73cc55cc918f4a65e022fd --cached -- QuickFiler/Controllers/EfcSelectionGuard.cs` + by a `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- QuickFiler/Controllers/EfcSelectionGuard.cs` run in the same task after `git add QuickFiler/Controllers/EfcSelectionGuard.cs`, whose only changed line is line 30. - [ ] [P6-T5] Redact host identity from every evidence artifact written so far, then prove it. Apply @@ -1199,10 +1233,10 @@ redirect the assignment. Reversing any of those orders produces a build that doe then `git commit -m "fix(637): normalize the breadcrumb producer and the string filing overload"` and write `evidence/other/p6-t6-commit.md`. A commit is required here because every Phase 7 and - Phase 8 gate is anchored to `b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD`, and an anchored diff + Phase 8 gate is anchored to `0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD`, and an anchored diff reports nothing for changes that are not yet committed. Acceptance: `EXIT_CODE: 0`; `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and - `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler QuickFiler.Test` + `git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler QuickFiler.Test` lists exactly these ten paths and no others: `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`, `QuickFiler/Controllers/EfcDataModel.cs`, @@ -1243,7 +1277,7 @@ again from the start. the write-mode discrimination that a read-only command cannot supply is provided by P7-T1's before-and-after porcelain pair. Then, in this same task, run `git add QuickFiler QuickFiler.Test` and `git commit -m "style(637): apply csharpier formatting before the coverage gates"`, so that - every subsequent `b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD` diff describes the same file + every subsequent `0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD` diff describes the same file contents the P7-T5 build measured. If nothing changed, record that the commit was a no-op and that the tree already matched `HEAD`. Record the commit result in `evidence/qa-gates/p7-t2-csharpier-check.md`. @@ -1286,11 +1320,11 @@ again from the start. production files this plan touches — `QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs`, `QuickFiler\Controllers\EfcDataModel.cs` and `QuickFiler\Controllers\EfcDataModel.FilingStem.cs` — and in the same task run - `git diff -U0 b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/EfcDataModel.cs QuickFiler/Controllers/EfcDataModel.FilingStem.cs` + `git diff -U0 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/EfcDataModel.cs QuickFiler/Controllers/EfcDataModel.FilingStem.cs` to enumerate the added line numbers from the hunk headers. The base anchor - `b9476588e0e49e113c73cc55cc918f4a65e022fd` is the post-merge, pre-change baseline P0-T8 proved - clean, so the added-line set this diff produces contains only lines this plan added and none of - issue #638's. Re-derive the + `0eda184ca0009bc79ac9b7146897270c17c095fa` is the post-merge, pre-change baseline P0-T8 proved + clean, so the added-line set this diff produces contains only lines this plan added and none + of issue #638's or issue #644's. Re-derive the `ToFilingStemOrVerbatim` line range against the post-format working tree in this same task, recording the declaration line and the closing-brace line, and record both that range and the range `evidence/regression-testing/p4-t2-helper-shape.md` recorded, stating whether they differ. @@ -1664,9 +1698,9 @@ is checked off before its cited evidence exists. Exactly one criterion is checke this task, in this order. First the porcelain companion, `git status --porcelain -- QuickFiler QuickFiler.Test UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. Second - `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- QuickFiler QuickFiler.Test`. + `git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler QuickFiler.Test`. Third - `git diff --name-only b9476588e0e49e113c73cc55cc918f4a65e022fd..HEAD -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. + `git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. Write `evidence/qa-gates/p8-t30-scope-boundary.md` recording all three outputs verbatim. The porcelain companion is required because a name-listing diff enumerates tracked changes only and never reports an untracked path, so the two diffs alone cannot fail on a file this plan created From 23a0c93405a2a858218beb06234d33ef9819aa6c Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Sun, 30 Aug 2026 08:13:08 -0400 Subject: [PATCH 06/16] docs(637): close the round-1 preflight delta in the atomic plan Six defects closed. D1: Phase 8's reconciliation task described a superseded spec.md, narrating corrections a prior spec revision had already applied and asserting a spec.md edit set no task in this plan performs; replaced with a verification list and a deliberately-uncorrected list. D2: the coverage gate could not fail correctly, because the wrapper throws on a sub-80 line rate independently of test failures; the exit code is now judged separately with an explicit COVERAGE_FLOOR_THROW branch. D3: P7-T7 and P8-T29 demanded non-zero hits for lines that carry no Cobertura node; now scoped to node-bearing lines with a non-vacuity floor. D4: repo-wide format drift outside the two owned trees is now detected at P0-T12 and at P7-T1 rather than first surfacing at P8-T30. D5 and D6 are citation accuracy. The planner's sibling-region sweep found three further sites carrying D1's false claim that the delta did not name, including a P1-T7 acceptance condition that would have required the executor to record that AC25 states 424 when it states 485. Refs #637 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --- .claude/agent-memory/atomic-planner/MEMORY.md | 1 + ...ject_637_r6_superseded_spec_claim_seams.md | 75 ++++++ .../plan.2026-08-29T12-20.md | 228 +++++++++++------- 3 files changed, 220 insertions(+), 84 deletions(-) create mode 100644 .claude/agent-memory/atomic-planner/project_637_r6_superseded_spec_claim_seams.md diff --git a/.claude/agent-memory/atomic-planner/MEMORY.md b/.claude/agent-memory/atomic-planner/MEMORY.md index 66eeee394..965f7df33 100644 --- a/.claude/agent-memory/atomic-planner/MEMORY.md +++ b/.claude/agent-memory/atomic-planner/MEMORY.md @@ -19,6 +19,7 @@ - [#677 R1–R8](project_677_keyboard_focus_leak_plan_seams.md) — ctor param REJECTED (5 reflection-arity tests); typed harness for compile-red; internal 9-arg ctor, never ambient SetSynchronizationContext; per-file non-vacuity floors - [#635](project_635_reflective_caller_audit_plan_seams.md) — evidence-only audit: tracked plan inflates its own sweep; scan hits its own pattern list; spec said six sites, tree has eight; pathspec breadth inflates a count - [#440 R1–R4](project_440_breadcrumb_left_arrow_plan_seams.md) — deletion-only change voids a diff-derived changed-line gate; `(Rebuild target(s))`, NOT `(Rebuild target)`; `.csharpierignore` matches the `.cobertura.xml` suffix; `.dotnet-sdk` IS gitignored (`.gitignore:350`); cite an AC by sentence only after counting its sentences; `Include` resolves against the declaring project's dir +- [#637 R6](project_637_r6_superseded_spec_claim_seams.md) — plan narrates spec edits it never performs; delta's stale-site list short by 3 (P1-T7 demanded a falsehood); wrapper throws on 2 conditions; Cobertura node-free lines; repo-wide format drift outside the staged pathspec - [#637 R2–R5](project_637_selectrow_rooted_path_plan_seams.md) — `docs/features/active` operand hits 121 sibling evidence files; the untracked-440 claim was FALSE for the agent worktree; blanket `-F` breaks every regex; pre-format range consumed post-format; probe with no branch for the outcome that occurs; set-difference file named by recency; R5 re-anchor on the merge commit, `#NNN` at column 0, CRLF round-trip check - [#644](project_644_ac16_referral_revision_seams.md) — AC's named instrument prints no figure; substitute's noise (~0.028pp) exceeds the 0.01pp delta; check-off task → referral task pinning the UNCHECKED state - [#644 cycle 2](project_644_cycle2_sweep_gate_evasion_seams.md) — rewording out of a detector's match set is gate evasion; SHA-256 pair for untracked prior artifacts; bound the porcelain `??` scope diff --git a/.claude/agent-memory/atomic-planner/project_637_r6_superseded_spec_claim_seams.md b/.claude/agent-memory/atomic-planner/project_637_r6_superseded_spec_claim_seams.md new file mode 100644 index 000000000..b454eb87f --- /dev/null +++ b/.claude/agent-memory/atomic-planner/project_637_r6_superseded_spec_claim_seams.md @@ -0,0 +1,75 @@ +--- +name: project-637-r6-superseded-spec-claim-seams +description: Issue #637 preflight round 6 — a "the spec already carries X" correction leaves the old figure attributed to the corrected criterion at plan sites the delta did not name; plus a two-throw coverage wrapper, Cobertura node-free lines, and repo-wide format drift outside the staged pathspec. +metadata: + type: project +--- + +Preflight round-6 seams from the issue #637 atomic plan (breadcrumb `SelectRow` emits a rooted path). +Complements [[project-637-selectrow-rooted-path-plan-seams]]. + +**A plan can narrate spec edits it never performs.** Phase 8 of this plan only flips `- [ ]` to +`- [x]`; no task edits `spec.md` prose. Yet six tasks and one tree-observation block asserted that +this plan "corrected", "reworded" or "extended" acceptance criteria — corrections a *prior* `spec.md` +revision had already applied. An executor would have recorded a non-discrepancy under a heading +asserting a discrepancy. +**Why:** the plan and the spec were revised in separate rounds; the plan kept the narration of a fix +whose object had moved into the spec's own history. +**How to apply:** before writing "this plan corrects AC*n* in `spec.md`", grep the plan for a task +that actually writes that file. If the only write is a checkbox flip, restate every such claim as a +verification ("AC*n* already carries X, re-verified against the merged tree") and split the +reconciliation artifact into an already-corrected list and a still-stale list. + +**The delta's own site list will be short.** The round-6 delta named `spec.md:401` and `:414-416` as +the surviving 424-line sites; the tree also carries 424 at `:582` and `:710`. More importantly, after +rewriting P8-T25 and tree-observation 1 to say "AC25's parenthetical already reads 485", **three** +other plan sites still said AC25 states 424 — tree observation 1's own preceding sentence, P1-T7's +acceptance ("the artifact records that `spec.md`'s implementation table **and AC25** state 424"), and +P8-T25's own preceding clause. P1-T7's was an acceptance condition demanding the executor record a +falsehood. +**How to apply:** after applying any "the spec already carries the corrected figure" edit, grep the +plan for the OLD numeral and for the criterion's name, and fix every site that still attributes the +old figure to the corrected criterion. See [[acceptance-edits-must-be-false-before-true-after]]. + +**`Invoke-MSTestWithCoverage.ps1` throws on two independent conditions.** Non-zero inner vstest exit +at `Invoke-MSTestWithCoverage.ps1:235-237`, and a repository line rate below 80 at +`Invoke-MSTestWithCoverage.Helpers.ps1:487-489` (called at `:341`). A clause reading "when the +baseline failure set is empty, EXIT_CODE 0 and 0 failed are required" is therefore unsatisfiable in +exactly the state the plan's own `BASELINE BELOW FLOOR` branch exists for. Judge failures and exit +code separately, keyed on the emitted literal `is below the required 80% threshold.`, and record +`ExpectedExitCode: 1` on the floor-throw branch. +Consequence worth stating in the plan: the threshold assertion at `:341` runs BEFORE the +post-processed write-back at `:343`, so on a floor throw the on-disk Cobertura file is the raw +dotnet-coverage output. Downstream tasks that re-apply `ConvertTo-KoverageCoberturaXml` in memory are +unaffected; a task that reads the file expecting post-processed content is not. +See [[reference-invoke-mstest-with-coverage-script]]. + +**A mandated literal may legitimately contain `%` or angle brackets.** `is below the required 80% +threshold.` and `<line>` both trip the wrap-tolerant rule's placeholder character list, so the plan +gate skips them. They are real emitted literals, not command shapes. Keep them and say so in the +handoff report rather than substituting a paraphrase that no run emits. + +**Cobertura emits a `<line>` node only for a sequence point.** "Every line inside the helper's range +has non-zero hits" is unsatisfiable, because XML documentation lines, the signature and blank lines +carry no node and no `hits` attribute. Correct shape: assert over the lines that DO carry a node, +require the artifact to enumerate and classify the node-free lines, and add "at least one line in the +range carries a node" so the clause cannot pass vacuously on a range with no coverage rows at all. +See [[async-state-machine-coverage-aggregation]] for the sibling aggregation trap. + +**A repo-wide write-mode formatter plus a narrow staging pathspec hides drift until the final gate.** +P7-T1 ran `csharpier format .` over the whole repository but observed porcelain only over +`QuickFiler`, `QuickFiler.Test` and the feature folder; P7-T2 staged only the two QuickFiler trees; +P8-T30's scope gate spanned nine trees. Pre-existing drift under `UtilitiesCS`, `TaskMaster`, +`ToDoModel`, `Tags` or `TaskVisualization` would have been repaired, never committed, and would have +made the final gate unsatisfiable with no earlier detector. +**How to apply:** the condition is measurable at the read-only baseline — add an +`OUT_OF_SCOPE_FORMAT_DRIFT:` halt branch to the Phase 0 `csharpier check` task, and give the +write-mode format task its own porcelain span over exactly the trees its staging pathspec omits. +See [[repo-wide-csharpier-format-breaks-zero-diff-acs]] and +[[observation-scope-must-match-blast-radius]]. + +**An insertion into a non-SDK csproj shifts every later `<Compile Include>` citation.** P2-T6 inserts +after `QuickFiler.Test/QuickFiler.Test.csproj:64`, so P2-T7's "already registered at `:114`" is stale +by the time P2-T7 runs (`:115`). Write both values with the event that separates them. Sweep the same +file's other cited lines (`:116`, `:133`) for acceptance conditions that read them post-insertion — +here none did. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index e198a1282..da35aa266 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -391,10 +391,10 @@ These were re-derived against the working tree and one disagrees with `spec.md`. no downstream artifact inherits a wrong figure. 1. `QuickFiler/Controllers/EfcDataModel.cs` is **485** lines on the merged tree — 423 at the - `ecdb1c84` planning base, and 424 in the spec's implementation table and in AC25's parenthetical. + `ecdb1c84` planning base, and 424 in the spec's implementation table at `spec.md:401`. Headroom to the 500-line limit is **15**. This is what forces the change-B file split described under "Scope". AC25's binding clause ("at or under 500 lines") is unaffected, and AC25's - parenthetical figure is corrected to 485 in `spec.md`. + parenthetical already reads 485 in `spec.md`. 2. The composition test `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` spans `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:167-214`; AC23 and the spec cite `:167-213`. The closing brace is at 214. No behavioral consequence. @@ -406,7 +406,7 @@ no downstream artifact inherits a wrong figure. search returns **23** lines across **6** files, and the syntax-anchored search returns **10** lines across **5** files: 3 declarations and 7 call sites, leaving 13 residual non-member textual references. Research section 6 says 16 lines across 6 files and `spec.md` says 16 across 5; both - describe the pre-merge tree. AC16's counts and citations are corrected in `spec.md`; P1-T2 and + describe the pre-merge tree. AC16 already carries the measured counts and citations; P1-T2 and P8-T16 carry the measured figures. The one file the stem search reaches that the syntax-anchored search does not is `QuickFiler.Test/Controllers/EfcHomeControllerTests.cs`, whose single match at `:55` is a comment naming `MoveToFolderAsync` and not a member reference. @@ -544,6 +544,13 @@ no downstream artifact inherits a wrong figure. exclude exactly the paths in that list and nothing else. The artifact also records whether the invocation produced a CSharpier result at all, which is the proof that P0-T10's tool restore succeeded. + If the `BASELINE_FORMAT_DRIFT` list names any path outside `QuickFiler` and `QuickFiler.Test`, + record `OUT_OF_SCOPE_FORMAT_DRIFT:` with that sub-list, stop, and report to the orchestrator + before proceeding to P0-T13. P7-T1's repo-wide write-mode format run would repair those paths, + P7-T2 stages only `QuickFiler QuickFiler.Test` so the repair would never be committed, and + P8-T30's porcelain span over the nine audited trees would then be non-empty and that task + unsatisfiable. Repairing pre-existing drift in trees this plan does not own is outside this + plan's scope and requires an explicit orchestrator decision. - [ ] [P0-T13] Capture the baseline analyzer build. Run `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; "EXIT_CODE=$LASTEXITCODE"'` and write `evidence/baseline/p0-t13-msbuild-analyzers.md`. Acceptance: `EXIT_CODE: 0`; the @@ -699,8 +706,9 @@ second, independently constructed search. No number in this phase is verified by `EfcSelectionGuard.cs` is 79; `BreadcrumbBridgeRouterIssue439Tests.cs` is 694; `EfcDataModelIssue614Tests.cs` is 123; `EfcSelectionGuardTests.cs` is 296; the `Test-Path` result for `QuickFiler\Controllers\EfcDataModel.FilingStem.cs` is `False` and is recorded as the - baseline for that path; and the artifact records that `spec.md`'s implementation table and AC25 - state 424 for `EfcDataModel.cs`, that the `ecdb1c84` planning base had 423, and that the merged + baseline for that path; and the artifact records that `spec.md`'s implementation table at + `spec.md:401` states 424 for `EfcDataModel.cs` while AC25 at `spec.md:977` already states 485, + that the `ecdb1c84` planning base had 423, and that the merged tree value 485 governs, leaving 15 lines of headroom to the 500-line limit. - [ ] [P1-T8] Re-derive the single pinning assertion (AC20: exactly 1 existing assertion changes). Construction 1, on the assertion form: @@ -854,7 +862,8 @@ redirect the assignment. Reversing any of those orders produces a build that doe identifiers" section, reaching `EfcDataModel.ToFilingStemOrVerbatim` through the existing `InternalsVisibleTo("QuickFiler.Test")` at `QuickFiler/Properties/AssemblyInfo.cs:5`. The eight existing `ToArchiveRelativeStem` tests in the file are not modified. The file is already - registered at `QuickFiler.Test/QuickFiler.Test.csproj:114`, so no new `Compile Include` is + registered in `QuickFiler.Test/QuickFiler.Test.csproj` — at `:114` on the pre-change tree, and + at `:115` once P2-T6 has inserted its line above it — so no new `Compile Include` is required. The declaration line this task creates is ` public class EfcDataModelIssue637Tests`, matching the form of the existing declaration at `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs:16`. The acceptance search below @@ -1269,6 +1278,14 @@ again from the start. changed or a path listed in the `BASELINE_FORMAT_DRIFT` section of `evidence/baseline/p0-t12-csharpier-check.md`. A path that is in neither set means the repo-wide format pass touched unrelated source and must be reported to the orchestrator before proceeding. + In the same task, run + `git status --porcelain -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization` + after the format run and record its output verbatim. That span must produce no output. It is + required because this task's other two porcelain spans are scoped to `QuickFiler`, + `QuickFiler.Test` and this feature's folder, so a repo-wide format rewrite in any of these seven + trees is invisible to them and would first surface at P8-T30, after the commit that could have + carried it. Any line here means the repo-wide format pass touched a tree this plan does not own; + stop and report to the orchestrator. - [ ] [P7-T2] Verify the format. Run `pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"'` and write `evidence/qa-gates/p7-t2-csharpier-check.md`. Acceptance: `EXIT_CODE: 0`, and the captured @@ -1302,7 +1319,18 @@ again from the start. test assemblies matches the number recorded in `evidence/baseline/p0-t15-mstest-coverage.md`; the post-change failing set is a subset of the `BASELINE_FAILURE_SET` recorded there; no test that passed in the baseline is failing now; the artifact names every baseline failure that is still - failing; and when `BASELINE_FAILURE_SET` is empty, `EXIT_CODE: 0` and 0 failed are required. + failing; and when `BASELINE_FAILURE_SET` is empty, 0 failed is required. The exit code is judged + separately, because the wrapper throws on two independent conditions: a non-zero inner vstest + exit (`Invoke-MSTestWithCoverage.ps1:235-237`) and a repository line rate below 80 percent + (`Invoke-MSTestWithCoverage.Helpers.ps1:487-489`, called at `:341`). When the captured output + contains the literal `is below the required 80% threshold.`, the artifact records + `COVERAGE_FLOOR_THROW: yes` together with the printed percentage, records `ExpectedExitCode: 1`, + and P7-T8's `BASELINE BELOW FLOOR` branch governs; the on-disk + `coverage\p7-t5-postchange.cobertura.xml` is then the raw dotnet-coverage output, because the + threshold assertion at `:341` precedes the post-processed write-back at `:343`, and P7-T6 and + P7-T7 re-apply `ConvertTo-KoverageCoberturaXml` in memory and are unaffected. When that literal + is absent, the artifact records `COVERAGE_FLOOR_THROW: no` and `EXIT_CODE: 0` is required + whenever `BASELINE_FAILURE_SET` is empty. `Output Summary:` additionally carries the six numeric `/coverage` attribute values and the derived line and branch percentages that P7-T6 reads, copied in once P7-T6 has produced them, because the plan contract requires the final-QC test-step artifact itself to carry the numeric @@ -1340,9 +1368,17 @@ again from the start. `evidence/qa-gates/p7-t7-changed-line-coverage.md`. Acceptance: the artifact lists, per file, the set of added line numbers and the set of line numbers with zero hits; the intersection of those two sets is empty for all three files; and, for - `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, every line - number inside the re-derived `ToFilingStemOrVerbatim` range has non-zero hits, which is the - new-code coverage requirement for the new helper stated in AC29; and, for the re-derived + `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, + every line number inside the re-derived `ToFilingStemOrVerbatim` range that carries a `<line>` + node in the post-change Cobertura document has non-zero hits, and the artifact separately + enumerates every line number inside that range that carries no `<line>` node, classifying each + as XML documentation, the method signature, a blank line, or a brace. That split is required + because Cobertura emits a `<line>` node only for a sequence point, so a documentation or + signature line has no `hits` attribute to read and an assertion over it could never be + satisfied. The artifact also records that at least one line inside the range carries a `<line>` + node, which is the observation that keeps this clause from passing vacuously on a range with no + coverage rows at all. Together these are the new-code coverage requirement for the new helper + stated in AC29; and, for the re-derived `ToFilingStemOrVerbatim` range, the artifact records the line nodes carrying `branch="True"` together with their `condition-coverage` values, and states that the `IsFullOutlookPath` conditional shows both branches taken — that is, a `condition-coverage` value of the form @@ -1549,8 +1585,8 @@ is checked off before its cited evidence exists. Exactly one criterion is checke `evidence/other/p6-t6-commit.md` for the two supporting edits the file split requires: the `partial` keyword on `EfcDataModel.cs:21` and the `<Compile Include>` registration in `QuickFiler/QuickFiler.csproj`. Acceptance: all four artifacts exist; the check-off record states - that AC11's text was corrected in `spec.md` to name the new declaring file and the corrected - assignment line 337, that the helper remains the member + that AC11's text already names the new declaring file and the assignment at line 337, + re-verified against the merged tree in P8-T32 list entry A3, that the helper remains the member `EfcDataModel.ToFilingStemOrVerbatim` because the new file is a partial of the same type, and that the split is the remedy `spec.md:414-416` authorizes for the 15-line headroom; AC11 is checked off. @@ -1576,28 +1612,32 @@ is checked off before its cited evidence exists. Exactly one criterion is checke diff and the 8 passing results including `ToArchiveRelativeStem_ArchiveRootItself_Throws`, and `evidence/regression-testing/p4-t6-nongoals-untouched.md` for the unmodified `MAPIFolder` overload at `EfcDataModel.cs:398-419` and its call to `ToArchiveRelativeStem` at line **407**. - Acceptance: all three artifacts exist; the check-off record states that AC15's citations were - corrected in `spec.md` from `:372-386`, `:336-357` and `:345` to the measured - `:421-448`, `:398-419` and `:407`; AC15 is checked off. + Acceptance: all three artifacts exist; the check-off record states that AC15 already carries + `:421-448`, `:398-419` and `:407`, re-verified against the merged tree in P8-T32 list + entries A5, A6 and A8; AC15 is checked off. - [ ] [P8-T16] AC16: re-run both P1-T2 searches against the post-change tree and write `evidence/qa-gates/p8-t16-movetofolder-family-post.md`. Acceptance: the syntax-anchored search still returns exactly **10** lines across **5** files, classified as 3 declarations and 7 call sites; the stem search still returns **23** lines across **6** files; no new overload and no - signature change appears; the check-off record states that AC16's counts and citations were - corrected in `spec.md` to those measured figures, and that the pre-merge figure of 16 lines is - superseded; AC16 is checked off. + signature change appears; the check-off record states that AC16 already carries those measured + figures, re-verified in P8-T32 list entry A9, and that the 16-line figure retained at + `spec.md:313` describes the pre-#638 tree and is recorded in P8-T32 list entry B6; AC16 is + checked off. - [ ] [P8-T17] AC17: cite `evidence/regression-testing/p4-t6-nongoals-untouched.md`. Acceptance: the artifact shows no hunk in the ranges 349 to 396 (`OpenOlFolderAsync` and `OpenFsFolderAsync`) or 398 to 448 (the `MAPIFolder` overload and `ToArchiveRelativeStem`), and no hunk in the protected range 271 to 297; it records that `Globals.Ol.ArchiveRootPath` occurs exactly once in the file, at line 284; and it records that the guarded read at 284 and the `UserDiagnosticAction(ArchiveRootUnavailableMessage)` degrade at 358 and 382, all introduced by - issue #638, are preserved unchanged. The check-off record states that AC17 was reworded in - `spec.md` for this reason: its original clause required that no `Globals.Ol.ArchiveRootPath` read + issue #638, are preserved unchanged. The check-off record states that AC17's current wording, + already present in + `spec.md`, is the one this plan can satisfy, and records the reason: its original clause + required that no `Globals.Ol.ArchiveRootPath` read gains a try/catch or a degrade, and issue #638 had already given the file's single read both, so that clause was false on the merged tree before this plan ran and no action this plan authorizes - could make it true. The reworded clause requires instead that #638's guarded read and degrade are - preserved unchanged, which is the property this plan can and does deliver; AC17 is checked off. + could make it true. AC17's current clause requires instead that #638's guarded read and degrade + are preserved unchanged, which is the property this plan can and does deliver; AC17 is checked + off. - [ ] [P8-T18] AC18: cite `evidence/regression-testing/p5-t5-single-assertion-change.md`, which records all three clauses. Acceptance: the artifact records the corrected assertion at line 165, the renamed method, and the narrowed two-line comment; AC18 is checked off. @@ -1635,10 +1675,11 @@ is checked off before its cited evidence exists. Exactly one criterion is checke listed file at or under 500 lines, including the new `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`; `BreadcrumbBridgeRouterIssue439Tests.cs` at or under 694 and therefore not grown; and the check-off record states that `EfcDataModel.cs` was - **485** lines before the change, not the 424 the spec's implementation table and AC25's - parenthetical stated, that AC25's parenthetical was corrected to 485 in `spec.md`, that the - resulting headroom of 15 lines is what forced the change-B file split, and that AC25 was extended - in `spec.md` to name the new file; AC25 is checked off. The bound is stated as "at or under" rather + **485** lines before the change, not the 424 the spec's implementation table at `spec.md:401` + still states, that AC25's parenthetical already reads 485 in `spec.md`, re-verified in + P8-T32 list entry A1, that the + resulting headroom of 15 lines is what forced the change-B file split, and that AC25 already + names the new file; AC25 is checked off. The bound is stated as "at or under" rather than "exactly" for the same reason it is in P7-T9: the figure is read after a write-mode formatter that can reduce a line count, and AC25 requires only non-growth. - [ ] [P8-T26] AC26: cite `evidence/regression-testing/p2-t13-compile-include-observed.md` for the @@ -1678,7 +1719,10 @@ is checked off before its cited evidence exists. Exactly one criterion is checke `evidence/qa-gates/p7-t8-coverage-delta.md`. Acceptance: the baseline capture is under `evidence/baseline/` and the post-change capture under `evidence/qa-gates/`, with no artifact written to `evidence/coverage/` or to any path under `artifacts/`; the changed-line uncovered - intersection is empty; every line of the new helper has non-zero hits, judged against the + intersection is empty; + every line of the new helper that carries a Cobertura `<line>` node has non-zero hits, with at + least one such node present, and P7-T7's enumeration of the range's node-free lines is cited, + judged against the `ToFilingStemOrVerbatim` range P7-T7 re-derived against the post-format working tree rather than against the pre-format range `evidence/regression-testing/p4-t2-helper-shape.md` recorded, with P7-T7's record of whether the two ranges differ cited here; the artifact @@ -1735,63 +1779,79 @@ is checked off before its cited evidence exists. Exactly one criterion is checke `evidence/qa-gates/p8-t31-ac-reconciliation.md`. Acceptance: both constructions report 30 checked and 0 unchecked; both agree; and the artifact records that an unscoped count of every `- [x]` and `- [ ]` line in `spec.md` would over-report by exactly 5, naming those five line numbers. -- [ ] [P8-T32] Record the spec-versus-tree discrepancies found during this work in - `evidence/other/p8-t32-spec-tree-discrepancies.md`. Most of these arise because `spec.md` was - authored against the tree before issue #638 merged, which changed `EfcDataModel.cs` and shifted - every line citation into it. Acceptance: the artifact records each item below, stating for each - the figure `spec.md` carries, the figure measured on the merged tree, and whether any acceptance - criterion's binding clause is affected. - 1. `EfcDataModel.cs` line count: `spec.md` states 424; the merged tree is **485**. Corrected in - AC25 by this plan. Binding clause ("at or under 500 lines") unaffected, but the derived - headroom of 15 lines is what forced the change-B file split. The implementation-table row at - `spec.md:401` and the headroom sentence at `spec.md:414-416` retain the 424 figure and its - derived 76-line headroom; they are recorded here rather than corrected, because no acceptance - criterion depends on them and the file split they authorize is taken on the measured figure. - 2. `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary`: AC23 cites `:167-213`; the - span is `:167-214`. Not corrected in `spec.md`. Binding clause unaffected. - 3. The `#499` clear-on-rebind block: `spec.md` cites `BreadcrumbBridgeRouter.cs:143-146`; the - block spans `:143-147`. Not corrected in `spec.md`. The write at `:145` and the read at - `:143` that AC24 names are exact, so AC24's binding clause is unaffected. - 4. The `string` overload declaration of `MoveToFolderAsync`: AC16 cited `EfcDataModel.cs:259`; - it is at **303**. Corrected in AC16 by this plan. - 5. The trash-sentinel comparison: AC5 cites `EfcDataModel.cs:272`; it is at **316**. Not - corrected in `spec.md`. AC5's binding clause is behavioral and is unaffected. - 6. The `DestinationOlStem` assignment in the `string` overload: AC11 cited - `EfcDataModel.cs:287`; it is at **337**. Corrected in AC11 by this plan. - 7. `OpenOlFolderAsync`: AC17 cited `:299-316`; it spans **349-372**. Corrected in AC17 by this - plan as part of that criterion's rewording. - 8. `OpenFsFolderAsync`: AC17 cited `:318-334`; it spans **374-396**. Corrected in AC17 by this - plan. - 9. The `MAPIFolder` overload: AC15 and AC16 cited `:336-357` and `:336`; it spans **398-419** - with its declaration at **398**. Corrected in both criteria by this plan. - 10. The `ToArchiveRelativeStem` call inside the `MAPIFolder` overload: AC15 cited `:345`; it is - at **407**. Corrected in AC15 by this plan. - 11. The `MoveToFolderAsync` delegation call inside the `MAPIFolder` overload: AC16 cited - `:346`; it is at **408**, and the call spans **408-414**. Corrected in AC16 by this plan. - 12. `ToArchiveRelativeStem` itself: AC15 cited `:372-386`; the declaration is at **434** and the - documented member spans **421-448**. Corrected in AC15 by this plan. - 13. The `MoveToFolder` family census: AC16 stated 3 declarations and 6 call sites, and `spec.md` - elsewhere states 16 stem lines across 5 files. The merged tree carries **23** stem lines - across **6** files and **10** syntax-anchored lines across **5** files, classified as 3 - declarations and **7** call sites. Corrected in AC16 by this plan. - 14. AC17's original clause required that no `Globals.Ol.ArchiveRootPath` read gains a new - try/catch or degrade. Issue #638 had already given the file's single read both, at - `EfcDataModel.cs:284` inside the `try` at 282-286, with the degrade at 358 and 382, so the - clause was false on the merged tree before this plan ran and no action this plan authorizes - could make it true. AC17 was therefore reworded in `spec.md` to require that #638's guarded - read and degrade are preserved unchanged. - 15. `spec.md:164-172` describes the whole `Globals.Ol.ArchiveRootPath` benign-degrade item as an - open non-goal owned by issue #695, and cites the two verbatim `DestinationOlStem` - assignments as `:308` and `:326`. The `EfcDataModel` half of that item shipped in issue #638 - and is no longer pending; the two assignments are at **364** and **388** and do remain - verbatim, so that half of the statement still holds. Not corrected in `spec.md`; this is - prose outside the acceptance criteria and no binding clause depends on it. - `spec.md` is edited by this plan only where an acceptance criterion would otherwise be - unsatisfiable or would name the wrong file — items 1, 4, 6, 7, 8, 9, 10, 11, 12, 13 and 14, plus - the file-split renaming in AC11, AC15 and AC25. Items 2, 3, 5 and 15 are recorded here and not - corrected, because each is a citation whose acceptance criterion remains satisfiable as written. - The acceptance-criteria count in `spec.md` is unchanged at 30: no criterion is added, removed, or - split by any of these corrections. +- [ ] [P8-T32] Record the spec-versus-tree reconciliation in + `evidence/other/p8-t32-spec-tree-discrepancies.md`. `spec.md` was authored against the tree + before issue #638 merged, and a prior revision of `spec.md` already applied the acceptance- + criteria corrections that shift required. This plan performs no `spec.md` text edit: its only + write to that file is the `- [ ]` to `- [x]` flip in P8-T1 through P8-T30. This task therefore + records two lists — criteria whose citations were already corrected before execution, verified + as still matching the tree, and citations that remain stale and are deliberately left uncorrected. + Acceptance: the artifact carries both lists below, and for every entry records the figure + `spec.md` carries, the figure measured on the merged tree, whether the two agree, and whether any + acceptance criterion's binding clause is affected. + + **List A — already corrected in `spec.md`; re-verified against the merged tree, all agree.** + Each entry is recorded as a verification, not as a correction this plan makes. + A1. AC25 (`spec.md:976-983`) states `EfcDataModel.cs` at **485** lines with **15** lines of + headroom; the tree is 485. This is what forced the change-B file split. + A2. AC16 (`spec.md:926`) cites the `string` overload declaration at `EfcDataModel.cs:303`; + the tree is 303. + A3. AC11 (`spec.md:900-906`) names `QuickFiler/Controllers/EfcDataModel.FilingStem.cs` at + `spec.md:900` and cites the `DestinationOlStem` assignment at `EfcDataModel.cs:337` at + `spec.md:903-904`; the tree is 337. + A4. AC17 (`spec.md:933`) cites `OpenOlFolderAsync` at `:349-372` and `OpenFsFolderAsync` at + `:374-396`; the tree is 349-372 and 374-396. + A5. AC15 and AC16 (`spec.md:918-920`, `:926`) cite the `MAPIFolder` overload at `:398-419` with + its declaration at `:398`; the tree is 398-419 and 398. + A6. AC15 (`spec.md:920`) cites the `ToArchiveRelativeStem` call inside that overload at `:407`; + the tree is 407. + A7. AC16 (`spec.md:928`) cites the `MoveToFolderAsync` delegation call at `:408`; the tree is + 408 and the call spans 408-414. + A8. AC15 (`spec.md:918`) cites `ToArchiveRelativeStem` at `:421-448` with its declaration at + `:434`; the tree is 421-448 and 434. + A9. AC16 (`spec.md:925-932`) states 3 declarations and 7 call sites, a family-stem search of + **23** lines across **6** files, and a syntax-anchored search of **10** lines across **5** + files; P1-T2 and P8-T16 measure exactly those figures. + A10. AC17 (`spec.md:933-937`) is already worded to require that issue #638's guarded read at + `EfcDataModel.cs:284` and its `UserDiagnosticAction(ArchiveRootUnavailableMessage)` degrade + at `:358` and `:382` are preserved unchanged. The clause a prior `spec.md` revision replaced + — that no `Globals.Ol.ArchiveRootPath` read gains a try/catch or a degrade — was false on + the merged tree before this plan ran, because #638 had already given the file's single read + both, so no action this plan authorizes could have made it true. The artifact records this + as the reason the current wording is the one that can be satisfied. + + **List B — still stale in `spec.md`; deliberately not corrected, because each remains + satisfiable as written or lies outside the acceptance criteria.** + B1. AC23 (`spec.md:967`) and the prose at `spec.md:783` cite + `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` at `:167-213`; the span is + `:167-214`. Binding clause unaffected. + B2. `spec.md:376` cites the `#499` clear-on-rebind block at `BreadcrumbBridgeRouter.cs:143-146`; + the block spans `:143-147`. The write at `:145` and the read at `:143` that AC24 names are + exact, so AC24's binding clause is unaffected. + B3. AC5 (`spec.md:874`) and the prose at `spec.md:127`, `:360`, `:635` and `:1021` cite the + `folderpath != "Trash to Delete"` comparison at `EfcDataModel.cs:272`; it is at **316**. + AC5's binding clause is behavioral and is unaffected. + B4. The implementation table at `spec.md:401`, the headroom sentence at `spec.md:414-416`, the + constraint sentence at `spec.md:582` and the census line at `spec.md:710` all retain the + **424**-line figure and its derived **76**-line headroom; the tree is 485 with 15. No + acceptance criterion depends on these four sites, and the file split they authorize is taken + on the measured figure, so they are recorded rather than corrected. + B5. The prose at `spec.md:119-120`, `:122`, `:284`, `:445`, `:469` and `:514` retains the + pre-#638 citations `:259-265` for the `string` overload declaration and `:287` for the + `DestinationOlStem` assignment; the tree gives `:303-309` for that declaration and `:337` + for that assignment. Recorded, not corrected. + B6. `spec.md:313` states the `MoveToFolder` family as 16 lines across 5 files; the merged tree + gives 23 stem lines across 6 files. This sentence sits in "Corrections to the research file" + and describes the pre-#638 tree. Recorded, not corrected. + B7. `spec.md:164-172` describes the whole `Globals.Ol.ArchiveRootPath` benign-degrade item as an + open non-goal owned by issue #695 and cites the two verbatim `DestinationOlStem` assignments + as `:308` and `:326`. The `EfcDataModel` half of that item shipped in issue #638 and is no + longer pending; the two assignments are at **364** and **388** and do remain verbatim, so + that half of the statement still holds. Prose outside the acceptance criteria; no binding + clause depends on it. + + The acceptance-criteria count in `spec.md` is unchanged at 30: this plan adds, removes and + splits no criterion, and edits no criterion's text. - [ ] [P8-T33] Final commit and clean tree. Run `git add docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` then From 8049ef7c4e0046a2cc57ba9ab24633ac8ef77f92 Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 10:10:37 -0400 Subject: [PATCH 07/16] (docs): plan and baseline artifacts --- .../baseline/p0-t10-dotnet-tool-restore.md | 8 + .../evidence/baseline/p0-t11-nuget-restore.md | 4 + .../baseline/p0-t12-csharpier-check.md | 11 + .../baseline/p0-t13-analyzer-backfill.md | 22 ++ .../baseline/p0-t13-msbuild-analyzers.md | 16 + .../p0-t13-msbuild-analyzers.retry.md | 10 + .../baseline/p0-t14-msbuild-nullable.md | 10 + .../baseline/p0-t15-mstest-coverage.md | 19 ++ .../evidence/baseline/p0-t6-spec-read.md | 4 + .../evidence/baseline/p0-t7-research-read.md | 8 + .../evidence/baseline/p0-t8-git-base.md | 20 ++ .../evidence/baseline/p0-t9-sdk-bootstrap.md | 4 + .../baseline/phase0-instructions-read.md | 16 + .../plan.2026-08-29T12-20.md | 284 ++++++++++-------- 14 files changed, 318 insertions(+), 118 deletions(-) create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t10-dotnet-tool-restore.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t11-nuget-restore.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t12-csharpier-check.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-analyzer-backfill.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-msbuild-analyzers.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-msbuild-analyzers.retry.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t14-msbuild-nullable.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t15-mstest-coverage.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t6-spec-read.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t7-research-read.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t8-git-base.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t9-sdk-bootstrap.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/phase0-instructions-read.md diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t10-dotnet-tool-restore.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t10-dotnet-tool-restore.md new file mode 100644 index 000000000..7a79cadf4 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t10-dotnet-tool-restore.md @@ -0,0 +1,8 @@ +Timestamp: 2026-08-31T09-34 +Command: pwsh -NoProfile -Command 'dotnet tool restore; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +Output: +Tool 'csharpier' (version '1.2.6') was restored. Available commands: csharpier + +Restore was successful. +EXIT_CODE=0 diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t11-nuget-restore.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t11-nuget-restore.md new file mode 100644 index 000000000..364ea242b --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t11-nuget-restore.md @@ -0,0 +1,4 @@ +Timestamp: 2026-08-31T09-35 +Command: pwsh -NoProfile -File scripts/vscode/Invoke-Restore.ps1 +EXIT_CODE: 0 +Output Summary: MSBuild Restore completed successfully with 0 warnings and 0 errors. 172 packages were installed for packages.config projects. Test-Path packages returned True. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t12-csharpier-check.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t12-csharpier-check.md new file mode 100644 index 000000000..d7d37a4f7 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t12-csharpier-check.md @@ -0,0 +1,11 @@ +Timestamp: 2026-08-31T09-36 +Command: pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +Output: +Checked 1562 files in 8003ms. +EXIT_CODE=0 + +BASELINE_FORMAT_DRIFT: +(none) + +Output Summary: CSharpier produced a check result and found no formatting drift. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-analyzer-backfill.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-analyzer-backfill.md new file mode 100644 index 000000000..df2e557e9 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-analyzer-backfill.md @@ -0,0 +1,22 @@ +Timestamp: 2026-08-31T10:00:00-04:00 +Command: nuget install Meziantou.Analyzer -Version 3.0.156 -OutputDirectory packages +EXIT_CODE: 0 +Output Summary: Installed the exact ignored package version required by the existing analyzer references. + +Command: nuget install Roslynator.Analyzers -Version 4.16.0 -OutputDirectory packages +EXIT_CODE: 0 +Output Summary: Installed the exact ignored package version required by the existing analyzer references. + +Analyzer reference enumeration: +- Matching `<Analyzer Include>` references: 80. +- Each listed project contains these five matching Include paths, each resolved from the project directory to the repository-relative path shown below and returned `Test-Path: True`. +- Projects: QuickFiler/QuickFiler.csproj; QuickFiler.Test/QuickFiler.Test.csproj; Tags/Tags.csproj; Tags.Test/Tags.Test.csproj; TaskMaster/TaskMaster.csproj; TaskMaster.Test/TaskMaster.Test.csproj; TaskTree/TaskTree.csproj; TaskTree.Test/TaskTree.Test.csproj; TaskVisualization/TaskVisualization.csproj; TaskVisualization.Test/TaskVisualization.Test.csproj; ToDoModel/ToDoModel.csproj; ToDoModel.Test/ToDoModel.Test.csproj; UtilitiesCS/UtilitiesCS.csproj; UtilitiesCS.Test/UtilitiesCS.Test.csproj; VBFunctions/VBFunctions.csproj; VBFunctions.Test/VBFunctions.Test.csproj. +- Resolved: packages/Meziantou.Analyzer.3.0.156/analyzers/dotnet/roslyn5.0/cs/Meziantou.Analyzer.dll; Test-Path: True. +- Resolved: packages/Roslynator.Analyzers.4.16.0/analyzers/dotnet/roslyn4.7/cs/Roslynator.CSharp.Analyzers.dll; Test-Path: True. +- Resolved: packages/Roslynator.Analyzers.4.16.0/analyzers/dotnet/roslyn4.7/cs/Roslynator_Analyzers_Roslynator.Common.dll; Test-Path: True. +- Resolved: packages/Roslynator.Analyzers.4.16.0/analyzers/dotnet/roslyn4.7/cs/Roslynator_Analyzers_Roslynator.Core.dll; Test-Path: True. +- Resolved: packages/Roslynator.Analyzers.4.16.0/analyzers/dotnet/roslyn4.7/cs/Roslynator_Analyzers_Roslynator.CSharp.dll; Test-Path: True. + +Command: git status --porcelain -- '*.csproj' '*/packages.config' 'packages' +EXIT_CODE: 0 +Output Summary: Empty output. The ignored analyzer bootstrap did not change tracked project files, package-policy files, or tracked package content. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-msbuild-analyzers.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-msbuild-analyzers.md new file mode 100644 index 000000000..91defc752 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-msbuild-analyzers.md @@ -0,0 +1,16 @@ +Timestamp: 2026-08-31T09-36 +Command: pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 1 +NULLABLE_OPT_IN_PROPERTY: absent +Output Summary: Rebuild target executed but failed with 0 warnings and 10 errors because the restored packages directory lacks the analyzer versions referenced by projects. + +BASELINE_BUILD_RED: +- VBFunctions.csproj and UtilitiesCS.csproj: CS0006 for ..\\packages\\Meziantou.Analyzer.3.0.156\\analyzers\\dotnet\\roslyn5.0\\cs\\Meziantou.Analyzer.dll. +- VBFunctions.csproj and UtilitiesCS.csproj: CS0006 for ..\\packages\\Roslynator.Analyzers.4.16.0\\analyzers\\dotnet\\roslyn4.7\\cs\\Roslynator.CSharp.Analyzers.dll. +- VBFunctions.csproj and UtilitiesCS.csproj: CS0006 for ..\\packages\\Roslynator.Analyzers.4.16.0\\analyzers\\dotnet\\roslyn4.7\\cs\\Roslynator_Analyzers_Roslynator.Common.dll. +- VBFunctions.csproj and UtilitiesCS.csproj: CS0006 for ..\\packages\\Roslynator.Analyzers.4.16.0\\analyzers\\dotnet\\roslyn4.7\\cs\\Roslynator_Analyzers_Roslynator.Core.dll. +- VBFunctions.csproj and UtilitiesCS.csproj: CS0006 for ..\\packages\\Roslynator.Analyzers.4.16.0\\analyzers\\dotnet\\roslyn4.7\\cs\\Roslynator_Analyzers_Roslynator.CSharp.dll. + +Observed restored package directories: +- Meziantou.Analyzer.3.0.174 +- Roslynator.Analyzers.4.16.1 diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-msbuild-analyzers.retry.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-msbuild-analyzers.retry.md new file mode 100644 index 000000000..516cb80e2 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t13-msbuild-analyzers.retry.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T10:01:17-04:00 +Command: pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +NULLABLE_OPT_IN_PROPERTY: absent +Output Summary: Build succeeded after the analyzer backfill. MSBuild reported 5 Warning(s), 0 Error(s), and executed Rebuild target(s). + +MSBuild final status line: Build succeeded. +Warning(s): 5 +Error(s): 0 +(Rebuild target(s)): observed. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t14-msbuild-nullable.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t14-msbuild-nullable.md new file mode 100644 index 000000000..e424c28ab --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t14-msbuild-nullable.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T10:01:51-04:00 +Command: pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +NULLABLE_OPT_IN_PROPERTY: absent +Output Summary: Build succeeded. MSBuild executed Rebuild target(s), with 5 warnings and 0 errors. + +MSBuild final status line: Build succeeded. +Warning(s): 5 +Error(s): 0 +(Rebuild target(s)): observed. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t15-mstest-coverage.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t15-mstest-coverage.md new file mode 100644 index 000000000..1cabbb854 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t15-mstest-coverage.md @@ -0,0 +1,19 @@ +Timestamp: 2026-08-31T10:07:03-04:00 +Command: pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\\p0-t15-baseline.cobertura.xml +EXIT_CODE: unavailable (the wrapper launcher exited while its coverage and test descendants continued; both verified descendants were terminated after they stopped progressing). +Output Summary: The wrapper discovered 9 test assemblies. It did not produce the required Cobertura document or coverage attributes. Before termination, the captured run reported at least eight 60-second test timeouts; total/passed/failed totals are unavailable because the run did not finish. + +BASELINE_EXECUTION_BLOCKED: +- Required output absent: coverage/p0-t15-baseline.cobertura.xml. +- The output-adjacent effective coverage configuration was cleaned up after the verified process-tree termination. +- Observed timed-out tests: + - InitializeSequentialAsync_ThroughThePumpHost_CompletesAndInitializesState + - CreateSequentialAsync_WithInjectedSeams_ReturnsAnInitializedController + - EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose + - InitializeGraphicsAsync_ThroughThePumpHost_CompletesAndAppliesDarkTheme + - CreateAsync_WithFaultingWebViewSeam_FaultsWithThatExceptionAfterInitializing + - EnsureDispatcher_ScopeDisposedTwice_IsIdempotent + - InitializeBool_ThroughThePumpHost_CompletesAndInitializesState + - Transaction_SecondCallerCannotInstallUntilTheFirstRestores +- The process command lines were verified as targeting this worktree before only their two dotnet-coverage process trees were terminated. +- P0-T15 remains unchecked. P0-T16 and P0-T17 cannot run without the required Cobertura document. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t6-spec-read.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t6-spec-read.md new file mode 100644 index 000000000..66666cc45 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t6-spec-read.md @@ -0,0 +1,4 @@ +Timestamp: 2026-08-31T09-33 +Command: Read spec.md and count markdown acceptance-criterion checkboxes within the ## Acceptance Criteria section only. +EXIT_CODE: 0 +Output Summary: 30 acceptance criteria found in ## Acceptance Criteria. The checkboxes at spec.md:54, spec.md:55, spec.md:56, spec.md:57, and spec.md:86 are outside that section and excluded. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t7-research-read.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t7-research-read.md new file mode 100644 index 000000000..e0f75a195 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t7-research-read.md @@ -0,0 +1,8 @@ +Timestamp: 2026-08-31T09-33 +Command: Read research/research.2026-08-29T12-30.md and spec.md corrections. +EXIT_CODE: 0 +Output Summary: spec.md governs where it conflicts with research. + +Corrections recorded by spec.md: +1. EfcDataModelTests.cs exists: QuickFiler.Test/Controllers/EfcDataModelTests.cs and EfcDataModelIssue614Tests.cs exist; the latter contains the dedicated eight-test ToArchiveRelativeStem suite. +2. MoveToFolder census: 16 matching lines occur across five files, not six. The correction also records that SelectedFolderPath occurs across three production files and six test files, not two production files and seven test files. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t8-git-base.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t8-git-base.md new file mode 100644 index 000000000..55d0c155b --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t8-git-base.md @@ -0,0 +1,20 @@ +Timestamp: 2026-08-31T09-33 +Command: git rev-parse --abbrev-ref HEAD +EXIT_CODE: 0 +Output: +agent-af95f0a8159ff28fa-wt-2026-08-31T08-39 + +Command: git rev-parse HEAD +EXIT_CODE: 0 +Output: +23a0c93405a2a858218beb06234d33ef9819aa6c + +Command: git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 +Output: + +Command: git status --porcelain -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 +Output: + +Output Summary: Branch matches the required executor branch. Both QuickFiler and QuickFiler.Test baseline checks produced no output. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t9-sdk-bootstrap.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t9-sdk-bootstrap.md new file mode 100644 index 000000000..7f00e4c75 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t9-sdk-bootstrap.md @@ -0,0 +1,4 @@ +Timestamp: 2026-08-31T09-33 +Command: pwsh -NoProfile -File scripts/vscode/Install-RepoDotNetSdk.ps1 +EXIT_CODE: 0 +Output Summary: Installed repo-local .NET SDK 8.0.205. Test-Path .dotnet-sdk/dotnet.exe returned True. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/phase0-instructions-read.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/phase0-instructions-read.md new file mode 100644 index 000000000..2036bbd01 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/phase0-instructions-read.md @@ -0,0 +1,16 @@ +Timestamp: 2026-08-31T09-33 +Policy Order: +1. AGENTS.md — standing instructions +2. AGENTS.md — Agent Code Change Policy +3. AGENTS.md — General Unit Test Policy +4. .agents/skills/csharp/SKILL.md + +Distinct Files Read: +- AGENTS.md +- .agents/skills/csharp/SKILL.md + +Recorded requirements: +- The file-size limit is 500 lines for production and test code. +- The C# toolchain loop is format, analyzer build, nullable build, then coverage-enabled test; it restarts when a step changes files or fails. +- Repository-wide line coverage remains at least 80 percent; new modules, classes, and methods target at least 90 percent; changed lines must not regress. +- Tests use MSTest, Moq, and FluentAssertions. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index da35aa266..38edcfaf6 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -3,9 +3,9 @@ - **Issue:** #637 - **Parent (optional):** none - **Owner:** drmoisan -- **Last Updated:** 2026-08-30T07-30 -- **Status:** Draft -- **Version:** 0.5 +- **Last Updated:** 2026-08-31T08-59 +- **Status:** Ready for Codex preflight +- **Version:** 0.6 - **Work Mode:** full-bug (from `issue.md`); `spec.md` is the sole acceptance-criteria source (AC1-AC30). ## Conventions (read before executing any task) @@ -226,20 +226,16 @@ Findings that this plan depends on: the percentage is assigned at `:486`, the enforcing comparison `if ($percentage -lt 80)` is at `:487`, and the `80%` message literal is at `:489`). -**Coverage-floor authority resolution.** Two repository documents state different repository-wide -coverage figures. `CLAUDE.md` states a floor of **80 percent line coverage on the testable -denominator**, together with a maintainer-ratified COM/VSTO/WinForms exemption. -`.claude/rules/general-unit-test.md` states 85 percent line and 75 percent branch. The conflict is -resolved here by authority ordering rather than by a new exception. `policy-compliance-order` ranks -`CLAUDE.md` first and `.claude/rules/general-unit-test.md` third, so `CLAUDE.md`'s 80 percent line -floor on the testable denominator is the binding repository-wide gate for this plan. It is also the -figure the runner itself enforces, at `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487`, so -the binding gate and the executed gate are the same number. The 85 percent line and 75 percent branch -figures in `.claude/rules/general-unit-test.md` are the stricter non-binding target and are superseded -by `CLAUDE.md` where they conflict; this plan still records the post-change figure against them as a -non-blocking observation, so the gap remains visible without gating the work. The change-scoped gates -— no changed line loses coverage, and the new helper is fully covered — are blocking regardless of -which repository-wide figure is quoted. P7-T8 states this resolution and applies it. +**Coverage-floor authority resolution.** `AGENTS.md` is the Codex repository-policy authority. Its +General Unit Test Policy requires repository-wide line coverage to remain at or above **80 percent**, +new modules, classes and methods to target at least **90 percent** coverage, and changed lines not to +lose coverage. `.agents/skills/csharp/SKILL.md` repeats those same three requirements. The repository +runner enforces the 80-percent repository floor at +`scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487`, so the binding repository-wide policy and +the executed gate use the same figure. The change-scoped gates remain independently blocking: no +changed line may lose coverage, and every Cobertura sequence point in the new helper must be covered, +with both decision outcomes demonstrated as P7-T7 specifies. P7-T8 states and applies this Codex +authority resolution. **Formatting observables.** `dotnet tool run csharpier format .` rewrites files and still exits 0, so its exit code alone proves nothing. The discriminating observation used by this plan is therefore a @@ -284,6 +280,21 @@ count over-reports by exactly five. Because no reusable tool is introduced, the obligation attached to such a tool does not arise; if a future revision introduces one, that obligation attaches and must be satisfied before the tool is used. +**Execution checkpoint boundaries.** This plan contains 107 atomic tasks. After completing and +checking off task 35 (`[P2-T10]`), task 70 (`[P7-T8]`), task 105 (`[P8-T31]`), and the final partial +interval at task 107 (`[P8-T33]`), the atomic executor must stop mutation and return the exact +`PROGRESS_COMMIT_REQUIRED` signal stated at that boundary. The orchestrator must then stage only the +completed interval's in-scope paths, collect canonical commit context through the repository +automation adapter, resolve and persist the routed `commit-steward` receipt, delegate the commit +message to that exact profile, create the commit, and record the task interval and resulting SHA in +`artifacts/orchestration/orchestrator-state.json` before execution resumes. No executor may mutate the +worktree while a boundary commit is being prepared. + +The existing HEAD-materialization points after `[P6-T6]`, `[P7-T2]`, and `[P7-T12]` remain necessary +because later gates use anchored `BASE..HEAD` diffs. They use the same orchestrator-controlled commit +protocol and do not replace the mandatory task-count boundaries. No atomic task invokes `git commit` +or selects its own commit message, and these boundaries add or renumber no tasks. + ## Scope In scope, exactly four changes: @@ -305,7 +316,7 @@ In scope, exactly four changes: **Why change B is split into a second file.** `spec.md:414-416` authorizes this: "if it does not, the helper moves to its own file rather than the 500-line limit being exceeded." That condition is met. `QuickFiler/Controllers/EfcDataModel.cs` is 485 lines on the merged tree, so its headroom to the -500-line limit in `.claude/rules/general-code-change.md` is 15 lines. The final helper needs roughly +500-line limit in the Agent Code Change Policy section of `AGENTS.md` is 15 lines. The final helper needs roughly 24 to 26 lines: five to eight lines of XML documentation that must state the contract, describe the second parameter, express the gate without naming `IsFullOutlookPath`, and express totality without using the character sequence `throw`; plus a body of about twelve lines, because the call @@ -446,34 +457,36 @@ no downstream artifact inherits a wrong figure. ### Phase 0 — Context, policy reads, and baseline capture -- [ ] [P0-T1] Read `CLAUDE.md` in full at the worktree root. Acceptance: the file is read in this - session before any other task in this phase, and its four-step C# toolchain command list is - quoted verbatim into the artifact written by P0-T5. -- [ ] [P0-T2] Read `.claude/rules/general-code-change.md` in full. Acceptance: the file is read, and - its 500-line file-size limit clause is quoted verbatim into the artifact written by P0-T5. -- [ ] [P0-T3] Read `.claude/rules/general-unit-test.md` in full. Acceptance: the file is read, and its - line-coverage and branch-coverage threshold sentence is quoted verbatim into the artifact written - by P0-T5. -- [ ] [P0-T4] Read `.claude/rules/csharp.md` in full. Acceptance: the file is read, and its statement - about the required test framework, mocking library and assertion library is quoted verbatim into - the artifact written by P0-T5. -- [ ] [P0-T5] Write `evidence/baseline/phase0-instructions-read.md` containing `Timestamp:`, - `Policy Order:` naming the four files in the order P0-T1 through P0-T4 read them, an explicit - bulleted list of those four file paths, and the four verbatim quotations required above. - Acceptance: the file exists and contains all of `Timestamp:`, `Policy Order:`, `CLAUDE.md`, - `general-code-change.md`, `general-unit-test.md`, `csharp.md`. -- [ ] [P0-T6] Read `spec.md` in full and write `evidence/baseline/p0-t6-spec-read.md` recording the +- [x] [P0-T1] Read `AGENTS.md` in full at the worktree root before any other task in this phase. + Acceptance: the standing-instructions entry `AGENTS.md — standing instructions` is recorded + first in the `Policy Order:` field of the artifact written by P0-T5. +- [x] [P0-T2] Re-read the `Agent Code Change Policy` section of `AGENTS.md`. Acceptance: the entry + `AGENTS.md — Agent Code Change Policy` is recorded second in P0-T5's `Policy Order:`, and the + artifact records the 500-line file-size limit and four-step toolchain-loop requirement. +- [x] [P0-T3] Re-read the `General Unit Test Policy` section of `AGENTS.md`. Acceptance: the entry + `AGENTS.md — General Unit Test Policy` is recorded third in P0-T5's `Policy Order:`, and the + artifact records the repository-wide 80-percent line floor, the at-least-90-percent target for + new modules, classes and methods, and the no-regression requirement for changed lines. +- [x] [P0-T4] Read `.agents/skills/csharp/SKILL.md` in full. Acceptance: that path is recorded fourth + in P0-T5's `Policy Order:`, and the artifact records the exact Codex C# format, analyzer, + nullable and coverage-enabled test sequence together with MSTest, Moq and FluentAssertions. +- [x] [P0-T5] Write `evidence/baseline/phase0-instructions-read.md` containing `Timestamp:`, + `Policy Order:`, an explicit ordered list of the four policy entries from P0-T1 through P0-T4, + and `Distinct Files Read:`. Acceptance: the file exists; `Policy Order:` contains exactly those + four ordered entries; and `Distinct Files Read:` contains exactly `AGENTS.md` and + `.agents/skills/csharp/SKILL.md`. +- [x] [P0-T6] Read `spec.md` in full and write `evidence/baseline/p0-t6-spec-read.md` recording the count of acceptance criteria found inside the `## Acceptance Criteria` section only. Acceptance: the recorded count is exactly 30, and the artifact also records that the five checkboxes at `spec.md:54`, `:55`, `:56`, `:57` and `:86` lie outside that section and are excluded. -- [ ] [P0-T7] Read `research/research.2026-08-29T12-30.md` in full and write +- [x] [P0-T7] Read `research/research.2026-08-29T12-30.md` in full and write `evidence/baseline/p0-t7-research-read.md` listing the two numbered corrections `spec.md` records under "Corrections to the research file", the second of which bundles two distinct file-count facts, and stating that `spec.md` governs where they conflict. Acceptance: the artifact names the `EfcDataModelTests.cs` existence correction, the `MoveToFolder` five-file correction, and the `SelectedFolderPath` three-production-file correction. -- [ ] [P0-T8] Record the branch and prove the base commit is a clean pre-change baseline. Run +- [x] [P0-T8] Record the current worktree branch and prove the base commit is a clean pre-change baseline. Run `git rev-parse --abbrev-ref HEAD`, `git rev-parse HEAD`, `git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler QuickFiler.Test`, and, as the porcelain companion to that name-listing diff, @@ -492,7 +505,7 @@ no downstream artifact inherits a wrong figure. Acceptance: the `git diff --name-only` invocation produces **no output at all**; the `git status --porcelain` invocation produces **no output at all**; and the recorded branch name is - `bug/breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. An ancestry check is + `agent-af95f0a8159ff28fa-wt-2026-08-31T08-39`. An ancestry check is deliberately not used here. `git merge-base --is-ancestor` exits 0 for any ancestor, including an ancestor that predates work this plan does not own, so it cannot fail in the state this task exists to detect; the empty-diff form fails as soon as any file under `QuickFiler` or @@ -502,7 +515,7 @@ no downstream artifact inherits a wrong figure. record `BASE MISMATCH` in the artifact together with the offending output, stop, and report to the orchestrator; do not proceed to P0-T9. -- [ ] [P0-T9] Bootstrap the repo-local .NET SDK with +- [x] [P0-T9] Bootstrap the repo-local .NET SDK with `pwsh -NoProfile -File scripts/vscode/Install-RepoDotNetSdk.ps1` and write `evidence/baseline/p0-t9-sdk-bootstrap.md`. Acceptance: `EXIT_CODE: 0`, and after the run the path `.dotnet-sdk/dotnet.exe` exists (record the result of `Test-Path .dotnet-sdk/dotnet.exe` as @@ -512,7 +525,7 @@ no downstream artifact inherits a wrong figure. `BOOTSTRAP_FAILED:`, stop, and report to the orchestrator; do not proceed to the next task and do not attempt a repair, because no toolchain command in this plan can run without the repo-local SDK. -- [ ] [P0-T10] Restore the pinned CSharpier tool with +- [x] [P0-T10] Restore the pinned CSharpier tool with `pwsh -NoProfile -Command 'dotnet tool restore; "EXIT_CODE=$LASTEXITCODE"'` and write `evidence/baseline/p0-t10-dotnet-tool-restore.md`. The manifest is `dotnet-tools.json` at the worktree root and pins `csharpier` `1.2.6`. Acceptance: `EXIT_CODE: 0`, and the captured stdout @@ -523,7 +536,7 @@ no downstream artifact inherits a wrong figure. error, which P0-T12 records. If the exit code is non-zero, record the captured output under a section headed `BOOTSTRAP_FAILED:`, stop, and report to the orchestrator; do not proceed to the next task and do not attempt a repair. -- [ ] [P0-T11] Restore NuGet packages with +- [x] [P0-T11] Restore NuGet packages with `pwsh -NoProfile -File scripts/vscode/Invoke-Restore.ps1` and write `evidence/baseline/p0-t11-nuget-restore.md`. This script resolves MSBuild through vswhere and runs `/t:Restore /p:RestorePackagesConfig=true`; it does not rewrite any `.csproj` HintPath. @@ -533,7 +546,7 @@ no downstream artifact inherits a wrong figure. orchestrator; do not proceed to the next task and do not attempt a repair, because an unrestored package graph produces CS0006 reference errors that are indistinguishable from real analyzer findings. -- [ ] [P0-T12] Capture the baseline format state **read-only** with +- [x] [P0-T12] Capture the baseline format state **read-only** with `pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"'` and write `evidence/baseline/p0-t12-csharpier-check.md`. The write-mode `format` command must not be run in Phase 0: repairing pre-existing drift before the baseline would either waive it silently @@ -551,19 +564,38 @@ no downstream artifact inherits a wrong figure. P8-T30's porcelain span over the nine audited trees would then be non-empty and that task unsatisfiable. Repairing pre-existing drift in trees this plan does not own is outside this plan's scope and requires an explicit orchestrator decision. -- [ ] [P0-T13] Capture the baseline analyzer build. Run +- [x] [P0-T13] Complete the baseline analyzer build after the recorded fresh-worktree analyzer + bootstrap recovery. Preserve the existing first-attempt evidence file + `evidence/baseline/p0-t13-msbuild-analyzers.md` unchanged: its `EXIT_CODE: 1` and + `BASELINE_BUILD_RED:` section record only `CS0006` diagnostics for the absent + `Meziantou.Analyzer.3.0.156` and `Roslynator.Analyzers.4.16.0` analyzer DLLs. Only when that + exact evidence condition holds, run `nuget install Meziantou.Analyzer -Version 3.0.156 + -OutputDirectory packages` and `nuget install Roslynator.Analyzers -Version 4.16.0 + -OutputDirectory packages`; both commands must exit `0`. This is a fresh-worktree bootstrap of + ignored `packages` contents only: do not edit any `.csproj`, `packages.config`, workflow, or + NuGet-policy file, and do not integrate `origin/main`. Record the two commands and their exit + codes in `evidence/baseline/p0-t13-analyzer-backfill.md`. Before the rebuild retry, enumerate + every `<Analyzer Include>` reference in every `*.csproj` whose path contains either of those + two exact package versions, resolve each path from its project directory, and record every + resolved analyzer DLL path and `Test-Path` result in that same artifact; every result must be + `True`. Then run `git status --porcelain -- '*.csproj' '*/packages.config' 'packages'`; it must + exit `0` with empty output, which is the proof that the bootstrap changed no tracked project or + package-policy surface. Immediately after those checks, retry the following existing baseline + analyzer command verbatim and write the retry result to + `evidence/baseline/p0-t13-msbuild-analyzers.retry.md`: `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; "EXIT_CODE=$LASTEXITCODE"'` - and write `evidence/baseline/p0-t13-msbuild-analyzers.md`. Acceptance: `EXIT_CODE: 0`; the - artifact records the MSBuild final status line and the `Warning(s)` and `Error(s)` counts as - printed; the captured output contains the literal `(Rebuild target(s))` at least once, which is - the per-project completion line MSBuild emits for the Rebuild target and is therefore the - discriminator against a skipped incremental Build; and the recorded `Command:` line does not - contain the solution-wide nullable opt-in property — record this as - `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. If the exit code is - non-zero, record the full diagnostic list under a section headed `BASELINE_BUILD_RED:`, stop, - and report to the orchestrator; do not proceed to the next task and do not attempt a repair, - because a pre-existing red baseline is outside this plan's scope. -- [ ] [P0-T14] Capture the baseline nullable build. Run + Acceptance: the first-attempt evidence remains unchanged; both NuGet commands and the status + command exit `0`; every referenced analyzer DLL is present; the status output is empty; and + the retry artifact records `EXIT_CODE: 0`, the MSBuild final status line, the `Warning(s)` and + `Error(s)` counts as printed, and `(Rebuild target(s))` at least once. The retry artifact's + `Command:` line must contain `/t:Rebuild` and `EnableNETAnalyzers=true` and + `EnforceCodeStyleInBuild=true`, and must not contain the solution-wide nullable opt-in property + — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. If + the evidence condition does not hold, either NuGet command fails, a referenced analyzer DLL is + absent, the status output is non-empty, or the retry exits non-zero, record the applicable + output under `BOOTSTRAP_FAILED:` or `BASELINE_BUILD_RED:` in the new recovery artifact, stop, + and report to the orchestrator; do not proceed to P0-T14 or attempt another repair. +- [x] [P0-T14] Capture the baseline nullable build. Run `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true; "EXIT_CODE=$LASTEXITCODE"'` and write `evidence/baseline/p0-t14-msbuild-nullable.md`. Acceptance: `EXIT_CODE: 0`; the captured output contains `(Rebuild target(s))`; and the recorded `Command:` line contains @@ -581,19 +613,18 @@ no downstream artifact inherits a wrong figure. failing test's fully qualified name (empty when the run passes). The file `coverage/p0-t15-baseline.cobertura.xml` exists after the run. `Output Summary:` additionally carries the six numeric `/coverage` attribute values and the derived line and branch - percentages that P0-T16 reads, copied in once P0-T16 has produced them, because the plan - contract requires the baseline test-step artifact itself to carry the numeric coverage - headline. The execution order for the pair is explicit and is not a deadlock: run this task's - command and write everything except the copied-back headline, leave P0-T15 **unchecked**, - execute P0-T16 in full, then return to P0-T15, write the copy-back into `Output Summary:`, and - check both tasks off together. + percentages. As a read-only P0-T15 substep, run the P0-T16 coverage-headline command against + `coverage\p0-t15-baseline.cobertura.xml`, record those values in this P0-T15 `Output Summary:`, + verify P0-T15, and check P0-T15 off before starting P0-T16. P0-T16 does not modify P0-T15. - [ ] [P0-T16] Read the baseline numeric coverage headline. Run `pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; $c = $d.SelectSingleNode("/coverage"); foreach ($a in @("line-rate","branch-rate","lines-covered","lines-valid","branches-covered","branches-valid")) { $a + "=" + $c.GetAttribute($a) }'` and write `evidence/baseline/p0-t16-coverage-headline.md`. Acceptance: `EXIT_CODE: 0`, and `Output Summary:` records all six numeric values, plus the derived baseline line-coverage percentage computed as `line-rate` multiplied by 100 and the derived branch percentage computed - as `branch-rate` multiplied by 100. These are the baseline figures the Phase 7 delta task - compares against. + as `branch-rate` multiplied by 100. Confirm that all six attributes and both derived + percentages equal the values already recorded in + `evidence/baseline/p0-t15-mstest-coverage.md`; P0-T16 does not modify that artifact or any + prior checklist state. These are the baseline figures the Phase 7 delta task compares against. - [ ] [P0-T17] Record the baseline uncovered-line sets for the two production files this plan changes. Run `pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; foreach ($f in @("QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcDataModel.cs")) { $u = @(); foreach ($c in $d.SelectNodes("//class")) { if ($c.GetAttribute("filename") -eq $f) { foreach ($l in $c.SelectNodes("./lines/line")) { if ([int]$l.GetAttribute("hits") -eq 0) { $u += [int]$l.GetAttribute("number") } } } }; $f + " uncovered=" + (($u | Sort-Object -Unique) -join ",") } '` @@ -909,6 +940,12 @@ redirect the assignment. Reversing any of those orders produces a build that doe set, record `BASELINE PROTECTS NOTHING`, stop, and report to the orchestrator: the invariant this task exists to protect would already be red before this plan ran, and no result here would distinguish a regression from that pre-existing state. + +**Progress-commit boundary after task 35.** Stop after checking off `[P2-T10]` and return +`PROGRESS_COMMIT_REQUIRED: P0-T1..P2-T10`. Do not begin `[P2-T11]` until the orchestrator has used +canonical commit context and the routed commit-steward profile, then recorded the completed interval's +commit SHA in the canonical checkpoint. + - [ ] [P2-T11] [expect-fail] Run the new router regression tests before the fix. Run `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; $asm = Join-Path (Get-Location).Path "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll"; & $vstest $asm /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue637Tests&TestCategory!=LiveOutlook" /Logger:trx "/ResultsDirectory:coverage\testresults\p2-t11"; "EXIT_CODE=$LASTEXITCODE"'` and write `evidence/regression-testing/p2-t11-router-tests-red.md` with `ExpectedExitCode: 1`. @@ -1237,13 +1274,15 @@ redirect the assignment. Reversing any of those orders produces a build that doe this task's own artifact is required for the same reason P7-T10 excludes its own: this artifact records its own two `Command:` lines, and those commands' patterns are the strings being searched for. No other evidence file of this feature is excluded. -- [ ] [P6-T6] Commit changes A through D. Run - `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` - then - `git commit -m "fix(637): normalize the breadcrumb producer and the string filing overload"` and - write `evidence/other/p6-t6-commit.md`. A commit is required here because every Phase 7 and - Phase 8 gate is anchored to `0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD`, and an anchored diff - reports nothing for changes that are not yet committed. Acceptance: `EXIT_CODE: 0`; +- [ ] [P6-T6] Prepare the changes-A-through-D HEAD-materialization boundary. Write + `evidence/other/p6-t6-commit.md`, check off this task, and return + `PROGRESS_COMMIT_REQUIRED: P2-T11..P6-T6` without invoking `git commit`. The orchestrator must + stage `QuickFiler`, `QuickFiler.Test`, and this feature folder; collect canonical commit context; + obtain the message from the routed commit-steward profile; create the commit; and record its SHA + before resuming Phase 7. This intermediate commit is required because every Phase 7 and Phase 8 + gate is anchored to `0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD`, and an anchored diff + reports nothing for changes that are not yet committed. Acceptance after the orchestrator resumes + execution: the checkpoint records the interval and a non-empty commit SHA; `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and `git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler QuickFiler.Test` lists exactly these ten paths and no others: @@ -1292,12 +1331,14 @@ again from the start. stdout is recorded verbatim. The exit code is the gate here rather than any summary wording, because `check` is read-only and returns non-zero exactly when some file would be reformatted; the write-mode discrimination that a read-only command cannot supply is provided by P7-T1's - before-and-after porcelain pair. Then, in this same task, run `git add QuickFiler QuickFiler.Test` - and `git commit -m "style(637): apply csharpier formatting before the coverage gates"`, so that + before-and-after porcelain pair. Record the boundary-ready state in + `evidence/qa-gates/p7-t2-csharpier-check.md`, check off this task, and return + `PROGRESS_COMMIT_REQUIRED: P7-T1..P7-T2` without invoking `git commit`. The orchestrator must + stage the in-scope paths, collect canonical commit context, obtain the message from the routed + commit-steward profile, create the commit, and record its SHA before P7-T3 begins. This ensures every subsequent `0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD` diff describes the same file - contents the P7-T5 build measured. If nothing changed, record that the commit was a no-op and - that the tree already matched `HEAD`. Record the commit result in - `evidence/qa-gates/p7-t2-csharpier-check.md`. + contents the P7-T5 build measures. If CSharpier changed no source, the evidence and plan check-off + still make the boundary non-empty; the artifact records that the source already matched `HEAD`. - [ ] [P7-T3] Analyzers. Run the P0-T13 command verbatim and write `evidence/qa-gates/p7-t3-msbuild-analyzers.md`. Acceptance: `EXIT_CODE: 0`; the output contains `(Rebuild target(s))`; the `Error(s)` count is 0; and the recorded `Command:` line contains @@ -1330,19 +1371,18 @@ again from the start. threshold assertion at `:341` precedes the post-processed write-back at `:343`, and P7-T6 and P7-T7 re-apply `ConvertTo-KoverageCoberturaXml` in memory and are unaffected. When that literal is absent, the artifact records `COVERAGE_FLOOR_THROW: no` and `EXIT_CODE: 0` is required - whenever `BASELINE_FAILURE_SET` is empty. - `Output Summary:` additionally carries the six numeric `/coverage` attribute values and the - derived line and branch percentages that P7-T6 reads, copied in once P7-T6 has produced them, - because the plan contract requires the final-QC test-step artifact itself to carry the numeric - coverage headline. The execution order for the pair is explicit and is not a deadlock: run this - task's command and write everything except the copied-back headline, leave P7-T5 **unchecked**, - execute P7-T6 in full, then return to P7-T5, write the copy-back into `Output Summary:`, and - check both tasks off together. + whenever `BASELINE_FAILURE_SET` is empty. `Output Summary:` additionally carries the six + numeric `/coverage` attribute values and the derived line and branch percentages. As a + read-only P7-T5 substep, run the P7-T6 coverage-headline command against + `coverage\p7-t5-postchange.cobertura.xml`, record those values in this P7-T5 `Output Summary:`, + verify P7-T5, and check P7-T5 off before starting P7-T6. P7-T6 does not modify P7-T5. - [ ] [P7-T6] Read the post-change numeric coverage headline. Run the P0-T16 command with the input path changed to `.\coverage\p7-t5-postchange.cobertura.xml` and write `evidence/qa-gates/p7-t6-coverage-headline.md`. Acceptance: `EXIT_CODE: 0`, and `Output Summary:` records all six numeric attribute values plus the derived line-coverage percentage and branch - percentage. + percentage. Confirm that all six attributes and both derived percentages equal the values + already recorded in `evidence/qa-gates/p7-t5-mstest-coverage.md`; P7-T6 does not modify that + artifact or any prior checklist state. - [ ] [P7-T7] Verify changed-line coverage. Run the P0-T17 command with the input path changed to `.\coverage\p7-t5-postchange.cobertura.xml` and with its file list extended to the three production files this plan touches — `QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs`, @@ -1403,27 +1443,30 @@ again from the start. `evidence/baseline/p0-t16-coverage-headline.md`; post-change coverage, copied from `evidence/qa-gates/p7-t6-coverage-headline.md`; and changed and new-code coverage, copied from `evidence/qa-gates/p7-t7-changed-line-coverage.md`. The artifact also states the coverage-floor - authority resolution this plan applies, in these terms: `CLAUDE.md` states a repository-wide floor - of at or above 80 percent line coverage on the testable denominator, together with a - maintainer-ratified COM/VSTO/WinForms exemption; `.claude/rules/general-unit-test.md` states 85 - percent line and 75 percent branch; `policy-compliance-order` ranks `CLAUDE.md` first and - `.claude/rules/general-unit-test.md` third, so `CLAUDE.md`'s 80 percent floor is the binding - repository-wide gate here and the 85/75 figures are the stricter non-binding target, superseded - where they conflict. The binding figure is also the one the runner enforces, at + authority resolution this plan applies, in these terms: the General Unit Test Policy in + `AGENTS.md` and `.agents/skills/csharp/SKILL.md` both require repository-wide line coverage at or + above 80 percent, new modules, classes and methods to target at least 90 percent coverage, and no + coverage regression on changed lines. The binding repository-wide figure is also the one the + runner enforces, at `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487`, so the gate this task applies and the gate the tooling applies are the same number. Acceptance: all three sections carry numeric values and none carries a placeholder; the post-change line-coverage percentage is at or above - **80**, and that clause is blocking; the artifact carries the authority statement above with its - two document citations; the artifact additionally records the post-change line and branch - percentages against the 85 percent and 75 percent figures as an explicitly **non-blocking** - observation, labelled `NON-BLOCKING TARGET:`, so the gap stays visible without gating the work; - and the changed-line section records an empty uncovered intersection. One exception applies to + **80**, and that clause is blocking; the artifact carries the authority statement above with both + Codex policy citations; the new helper meets the at-least-90-percent target through P7-T7's + stronger requirement that every emitted Cobertura sequence point has non-zero hits; and the + changed-line section records an empty uncovered intersection. One exception applies to the blocking clause and to nothing else: if the baseline figure recorded in `evidence/baseline/p0-t16-coverage-headline.md` is itself already below 80, the artifact records `BASELINE BELOW FLOOR`, reports that pre-existing condition to the orchestrator, and the binding requirement becomes that the post-change figure is at or above the recorded baseline figure. The change-scoped gates — no changed line loses coverage, and every line of the new helper is covered — remain blocking in every case, including under that exception. + +**Progress-commit boundary after task 70.** Stop after checking off `[P7-T8]` and return +`PROGRESS_COMMIT_REQUIRED: P2-T11..P7-T8`. Do not begin `[P7-T9]` until the orchestrator has used +canonical commit context and the routed commit-steward profile, then recorded the boundary SHA and all +intermediate HEAD-materialization SHAs within this task interval in the canonical checkpoint. + - [ ] [P7-T9] File-size audit, run after the formatter rather than before it, because CSharpier can change a file's line count. Run `pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\EfcDataModel.FilingStem.cs","QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcSelectionGuard.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs","QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs","QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }'` @@ -1504,19 +1547,19 @@ again from the start. artifact and `evidence/other/p6-t5-evidence-redaction.md`, which P6-T5 wrote earlier in the same feature tree. Omitting the second exclusion would make this gate unsatisfiable. No other evidence file of this feature is excluded. -- [ ] [P7-T12] Commit the QA evidence and any residual formatting result. Run - `git add QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` - then - `git commit -m "chore(637): final QC toolchain pass and coverage evidence"` and write - `evidence/other/p7-t12-commit.md`. Acceptance: `EXIT_CODE: 0`, and - `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and +- [ ] [P7-T12] Prepare the QA-evidence HEAD-materialization boundary. Write + `evidence/other/p7-t12-commit.md`, check off this task, and return + `PROGRESS_COMMIT_REQUIRED: P7-T9..P7-T12` without invoking `git commit`. The orchestrator must + stage `QuickFiler`, `QuickFiler.Test`, and this feature folder; collect canonical commit context; + obtain the message from the routed commit-steward profile; create the commit; and record its SHA + before Phase 8 begins. Acceptance after execution resumes: the checkpoint records the interval + and a non-empty commit SHA; `git status --porcelain -- QuickFiler QuickFiler.Test` produces no + output; and `git status --porcelain -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` lists at most this task's own evidence artifact and this plan file, with every other feature-folder path already in `HEAD`. Record - both outputs verbatim. This task's commit carries the Phase 7 evidence artifacts; the - formatting result itself is normally already in `HEAD` because P7-T2 committed it, so a - source-only no-op here is expected rather than exceptional. If the commit fails because nothing - changed at all, record that outcome and both `git status` results. + both outputs verbatim. The boundary commit carries the remaining Phase 7 evidence artifacts; the + formatting result itself is already in `HEAD` through the P7-T2 boundary. ### Phase 8 — Acceptance-criteria reconciliation @@ -1727,10 +1770,9 @@ is checked off before its cited evidence exists. Exactly one criterion is checke against the pre-format range `evidence/regression-testing/p4-t2-helper-shape.md` recorded, with P7-T7's record of whether the two ranges differ cited here; the artifact `evidence/qa-gates/p7-t8-coverage-delta.md` records either a post-change line-coverage - percentage at or above 80 — the binding repository-wide floor under the authority resolution - P7-T8 states, in which `policy-compliance-order` ranks `CLAUDE.md` above - `.claude/rules/general-unit-test.md`, so the latter's 85 and 75 figures are the non-binding - target and are recorded as such — or an explicit `BASELINE BELOW FLOOR` finding with the + percentage at or above 80 — the binding repository-wide floor in `AGENTS.md` and + `.agents/skills/csharp/SKILL.md` under the authority resolution P7-T8 states — or an explicit + `BASELINE BELOW FLOOR` finding with the post-change figure at or above the recorded baseline; the changed-line intersection is empty for all three production files, including the new `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`; the `IsFullOutlookPath` conditional in the @@ -1779,6 +1821,12 @@ is checked off before its cited evidence exists. Exactly one criterion is checke `evidence/qa-gates/p8-t31-ac-reconciliation.md`. Acceptance: both constructions report 30 checked and 0 unchecked; both agree; and the artifact records that an unscoped count of every `- [x]` and `- [ ]` line in `spec.md` would over-report by exactly 5, naming those five line numbers. + +**Progress-commit boundary after task 105.** Stop after checking off `[P8-T31]` and return +`PROGRESS_COMMIT_REQUIRED: P7-T9..P8-T31`. Do not begin `[P8-T32]` until the orchestrator has used +canonical commit context and the routed commit-steward profile, then recorded the boundary SHA and the +P7-T12 intermediate SHA within this task interval in the canonical checkpoint. + - [ ] [P8-T32] Record the spec-versus-tree reconciliation in `evidence/other/p8-t32-spec-tree-discrepancies.md`. `spec.md` was authored against the tree before issue #638 merged, and a prior revision of `spec.md` already applied the acceptance- @@ -1852,16 +1900,16 @@ is checked off before its cited evidence exists. Exactly one criterion is checke The acceptance-criteria count in `spec.md` is unchanged at 30: this plan adds, removes and splits no criterion, and edits no criterion's text. -- [ ] [P8-T33] Final commit and clean tree. Run - `git add docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` - then - `git commit -m "docs(637): reconcile acceptance criteria and record final evidence"` and write - `evidence/other/p8-t33-final-commit.md`. Acceptance: `EXIT_CODE: 0`, and - `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output; and +- [ ] [P8-T33] Finalise the last partial interval without invoking `git commit`. Write + `evidence/other/p8-t33-final-commit.md`, check off this task, verify that + `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output, record `git status --porcelain -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` - lists at most this task's own evidence - artifact and this plan file, with every other feature-folder path already in `HEAD`. Record - both outputs verbatim. The pathspec scoping is required because `.claude/` is tracked and + verbatim, and return `PROGRESS_COMMIT_REQUIRED: P8-T32..P8-T33`. Before review, the orchestrator + must stage this feature folder, collect canonical commit context, obtain the message from the + routed commit-steward profile, create the commit, record its SHA for the final partial interval, + and verify both status spans are empty. Before that boundary commit, the feature-folder status + may list only this task's evidence artifact and this plan file, with every other feature-folder + path already in `HEAD`. The pathspec scoping is required because `.claude/` is tracked and carries unrelated in-flight modifications that this plan must not commit, and because sibling feature folders under `docs/features/active` are owned by other work. This task runs long after planning, and the tracking state of a sibling folder under that parent directory is From c90e678e1e47eb3b6ef2e4643b22096a0df1b9f1 Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 10:39:57 -0400 Subject: [PATCH 08/16] test(quickfiler): add rooted filing-path regression coverage - Cover rooted archive targets, boundary rejections, and passthrough cases - Introduce the filing-stem seam and record P0 through P2 evidence Refs: #637 --- .../BreadcrumbBridgeRouterIssue637Tests.cs | 251 ++++++++++++++++++ .../Controllers/EfcDataModelIssue614Tests.cs | 70 +++++ QuickFiler.Test/QuickFiler.Test.csproj | 1 + .../Controllers/EfcDataModel.FilingStem.cs | 16 ++ QuickFiler/Controllers/EfcDataModel.cs | 4 +- QuickFiler/QuickFiler.csproj | 3 +- .../evidence/baseline/p0-t15-retry-1.md | 21 ++ .../baseline/p0-t16-coverage-headline.md | 4 + .../p0-t17-baseline-uncovered-lines.md | 7 + .../baseline/p1-t1-selection-family.md | 18 ++ .../baseline/p1-t2-movetofolder-family.md | 21 ++ .../p1-t3-selectedfolderpath-surface.md | 14 + .../baseline/p1-t4-deferral-records.md | 10 + .../p1-t5-toarchiverelativestem-tests.md | 5 + .../baseline/p1-t6-passthrough-tests.md | 11 + .../baseline/p1-t7-file-line-counts.md | 8 + .../baseline/p1-t8-pinning-assertion.md | 7 + .../p2-t10-issue638-preserved.md | 6 + .../p2-t8-msbuild-analyzers.md | 4 + .../p2-t9-msbuild-nullable.md | 5 + .../plan.2026-08-29T12-20.md | 85 +++--- 21 files changed, 531 insertions(+), 40 deletions(-) create mode 100644 QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs create mode 100644 QuickFiler/Controllers/EfcDataModel.FilingStem.cs create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t15-retry-1.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t16-coverage-headline.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t17-baseline-uncovered-lines.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t1-selection-family.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t2-movetofolder-family.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t3-selectedfolderpath-surface.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t4-deferral-records.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t5-toarchiverelativestem-tests.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t6-passthrough-tests.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t7-file-line-counts.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t8-pinning-assertion.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t10-issue638-preserved.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t8-msbuild-analyzers.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t9-msbuild-nullable.md diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs new file mode 100644 index 000000000..70e368b00 --- /dev/null +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using FluentAssertions; +using log4net; +using log4net.Appender; +using log4net.Repository.Hierarchy; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using QuickFiler.Controllers; +using QuickFiler.Viewers; +using UtilitiesCS; +using UtilitiesCS.OutlookObjects.Folder; + +namespace QuickFiler.Test.Controllers +{ + [TestClass] + public class BreadcrumbBridgeRouterIssue637Tests + { + private const string MailboxRoot = @"\\mailbox@example.com"; + private const string ArchiveRoot = @"\\mailbox@example.com\Archive"; + private Mock<IFolderHierarchyProvider> _provider; + private Mock<IBreadcrumbWebHost> _host; + private BreadcrumbBridgeRouter _router; + private MemoryAppender _appender; + + [TestInitialize] + public void Setup() + { + _provider = new Mock<IFolderHierarchyProvider>(); + _host = new Mock<IBreadcrumbWebHost>(); + _host.SetupGet(host => host.IsCoreInitialized).Returns(true); + _router = new BreadcrumbBridgeRouter( + _provider.Object, + _host.Object, + new BreadcrumbMessageCodec(), + new BreadcrumbHtmlRenderer(), + new BreadcrumbOutboundQueue(_host.Object) + ); + _appender = AttachMemoryAppender(typeof(BreadcrumbBridgeRouter)); + } + + [TestCleanup] + public void Cleanup() + { + DetachMemoryAppender(typeof(BreadcrumbBridgeRouter), _appender); + } + + [TestMethod] + public void RowSelected_ArchiveRootExactFilingTarget_IsNotSelected() + { + BindRows(ArchiveRoot, ArchiveRoot); + + Inbound(RowSelected(0)); + + _router.SelectedFolderPath.Should().BeNull(); + } + + [TestMethod] + public void RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection() + { + BindRows(ArchiveRoot, @"Clients\North", ArchiveRoot); + Inbound(RowSelected(0)); + + Inbound(RowSelected(1)); + + _router.SelectedFolderPath.Should().Be(@"Clients\North"); + } + + [TestMethod] + public void RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem() + { + BindRows(ArchiveRoot, ArchiveRoot + @"\Clients\North"); + + Inbound(RowSelected(0)); + + _router.SelectedFolderPath.Should().Be(@"Clients\North"); + } + + [TestMethod] + public void RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem() + { + const string configuredRoot = @"\\MAILBOX@EXAMPLE.COM\ARCHIVE\"; + const string target = @"\\mailbox@example.com\aRcHiVe\Clients\South"; + BindRows(configuredRoot, target); + + Inbound(RowSelected(0)); + + _router.SelectedFolderPath.Should().Be(@"Clients\South"); + } + + [TestMethod] + public void RowSelected_RelativeFilingTarget_CommitsTheValueVerbatim() + { + BindRows(ArchiveRoot, @"Clients\North"); + + Inbound(RowSelected(0)); + + _router.SelectedFolderPath.Should().Be(@"Clients\North"); + } + + [TestMethod] + public void RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim() + { + BindRows(ArchiveRoot, BreadcrumbRowBuilder.TrashRowText); + + Inbound(RowSelected(0)); + + _router.SelectedFolderPath.Should().Be(BreadcrumbRowBuilder.TrashRowText); + } + + [TestMethod] + public void RowSelected_OutOfRootRootedTarget_IsStillRejected() + { + BindRows(ArchiveRoot, @"\\other@example.org\Archive\Clients"); + + Inbound(RowSelected(0)); + + _router.SelectedFolderPath.Should().BeNull(); + } + + [TestMethod] + public void RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected() + { + BindRows(ArchiveRoot, @"\\mailbox@example.com\Archive2\Clients"); + + Inbound(RowSelected(0)); + + _router.SelectedFolderPath.Should().BeNull(); + } + + [TestMethod] + public void RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim() + { + const string target = @"\Archive\Clients\North"; + BindRows(@"\", target); + + Inbound(RowSelected(0)); + + _router.SelectedFolderPath.Should().Be(target); + } + + [TestMethod] + public void SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem() + { + BindRows(ArchiveRoot, ArchiveRoot + @"\Clients\North"); + + _router.SelectFirstRow(); + + _router.SelectedFolderPath.Should().Be(@"Clients\North"); + } + + private void BindRows(string archiveRoot, params string[] presentedRows) + { + foreach (string presentedTarget in presentedRows) + { + if (BreadcrumbRowBuilder.Classify(presentedTarget) != BreadcrumbRowKind.Suggestion) + { + continue; + } + + string hierarchyPath = ToHierarchyPath(presentedTarget, archiveRoot); + FolderTreeNodeKey key = Key(hierarchyPath); + _provider + .Setup(provider => + provider.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()) + ) + .ReturnsAsync(key); + _provider + .Setup(provider => + provider.GetAncestorChainAsync(key, It.IsAny<CancellationToken>()) + ) + .ReturnsAsync(Chain(hierarchyPath, archiveRoot)); + } + + _router + .BindRowsAsync( + presentedRows, + new[] { new FolderScore(presentedRows[0], 730, 0.73) }, + archiveRoot, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + } + + private static string ToHierarchyPath(string presentedTarget, string archiveRoot) + { + string boundRoot = archiveRoot.TrimEnd('\\', '/'); + if (boundRoot.Length == 0 || ArchiveStemContract.IsFullOutlookPath(presentedTarget)) + { + return presentedTarget; + } + + return boundRoot + "\\" + presentedTarget; + } + + private static IReadOnlyList<FolderBreadcrumbSegment> Chain(string hierarchyPath, string archiveRoot) + { + string boundRoot = archiveRoot.TrimEnd('\\', '/'); + var chain = new List<FolderBreadcrumbSegment>(); + if (boundRoot.Length != 0) + { + chain.Add(Segment(MailboxRoot, "Mailbox", true)); + chain.Add(Segment(boundRoot, "Archive", true)); + } + + chain.Add(Segment(hierarchyPath, "Target", false)); + return chain; + } + + private void Inbound(string json) + { + _router.ProcessInboundAsync(json).GetAwaiter().GetResult(); + } + + private static string RowSelected(int rowIndex) + { + return "{\"type\":\"rowSelected\",\"rowId\":\"row-" + rowIndex + "\"}"; + } + + private static FolderTreeNodeKey Key(string path) + { + return new FolderTreeNodeKey("archive-store", path, path); + } + + private static FolderBreadcrumbSegment Segment(string path, string name, bool hasChildren) + { + return new FolderBreadcrumbSegment(Key(path), name, path, hasChildren); + } + + private static MemoryAppender AttachMemoryAppender(Type targetType) + { + var appender = new MemoryAppender(); + appender.ActivateOptions(); + var hierarchy = (Hierarchy)LogManager.GetRepository(); + var logger = (Logger)hierarchy.GetLogger(targetType.FullName); + logger.Level = log4net.Core.Level.Debug; + logger.AddAppender(appender); + logger.Repository.Configured = true; + return appender; + } + + private static void DetachMemoryAppender(Type targetType, MemoryAppender appender) + { + var hierarchy = (Hierarchy)LogManager.GetRepository(); + var logger = (Logger)hierarchy.GetLogger(targetType.FullName); + logger.RemoveAppender(appender); + } + } +} diff --git a/QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs b/QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs index 6d9973905..e273032b8 100644 --- a/QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs +++ b/QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs @@ -120,4 +120,74 @@ public void ToArchiveRelativeStem_RepeatedAncestorSubstring_StripsOnlyThePrefix( stem.Should().Be(@"Clients\Archive\North"); } } + + [TestClass] + public class EfcDataModelIssue637Tests + { + private const string ArchiveRoot = @"\\mailbox@example.com\Archive"; + + [TestMethod] + public void ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem() + { + EfcDataModel + .ToFilingStemOrVerbatim(ArchiveRoot + @"\Clients\North", ArchiveRoot) + .Should() + .Be(@"Clients\North"); + } + + [TestMethod] + public void ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem() + { + EfcDataModel + .ToFilingStemOrVerbatim(@"\\MAILBOX@EXAMPLE.COM\aRcHiVe\Clients", ArchiveRoot) + .Should() + .Be("Clients"); + } + + [TestMethod] + public void ToFilingStemOrVerbatim_RelativeStem_ReturnsTheInputVerbatim() + { + EfcDataModel.ToFilingStemOrVerbatim(@"Clients\North", ArchiveRoot).Should().Be(@"Clients\North"); + } + + [TestMethod] + public void ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim() + { + EfcDataModel + .ToFilingStemOrVerbatim("Trash to Delete", ArchiveRoot) + .Should() + .Be("Trash to Delete"); + } + + [TestMethod] + public void ToFilingStemOrVerbatim_ArchiveRootExact_ReturnsTheInputVerbatimAndDoesNotThrow() + { + EfcDataModel.ToFilingStemOrVerbatim(ArchiveRoot, ArchiveRoot).Should().Be(ArchiveRoot); + } + + [TestMethod] + public void ToFilingStemOrVerbatim_OutOfRootRootedInput_ReturnsTheInputVerbatimAndDoesNotThrow() + { + const string candidate = @"\\other@example.org\Archive\Clients"; + EfcDataModel.ToFilingStemOrVerbatim(candidate, ArchiveRoot).Should().Be(candidate); + } + + [TestMethod] + public void ToFilingStemOrVerbatim_NullEmptyWhitespaceOrSeparatorOnlyAncestor_ReturnsTheInputVerbatim() + { + const string candidate = @"\\mailbox@example.com\Archive\Clients"; + + EfcDataModel.ToFilingStemOrVerbatim(candidate, null).Should().Be(candidate); + EfcDataModel.ToFilingStemOrVerbatim(candidate, string.Empty).Should().Be(candidate); + EfcDataModel.ToFilingStemOrVerbatim(candidate, " ").Should().Be(candidate); + EfcDataModel.ToFilingStemOrVerbatim(candidate, @"\").Should().Be(candidate); + } + + [TestMethod] + public void ToFilingStemOrVerbatim_NullOrEmptyCandidate_ReturnsTheInputVerbatim() + { + EfcDataModel.ToFilingStemOrVerbatim(null, ArchiveRoot).Should().BeNull(); + EfcDataModel.ToFilingStemOrVerbatim(string.Empty, ArchiveRoot).Should().BeEmpty(); + } + } } diff --git a/QuickFiler.Test/QuickFiler.Test.csproj b/QuickFiler.Test/QuickFiler.Test.csproj index db3b7f7a8..5dbb9b54a 100644 --- a/QuickFiler.Test/QuickFiler.Test.csproj +++ b/QuickFiler.Test/QuickFiler.Test.csproj @@ -62,6 +62,7 @@ <Compile Include="Controllers\BreadcrumbBridgeRouterIssue614Tests.cs" /> <Compile Include="Controllers\EfcSelectionGuardTests.cs" /> <Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.cs" /> + <Compile Include="Controllers\BreadcrumbBridgeRouterIssue637Tests.cs" /> <Compile Include="Viewers\BreadcrumbBridgeCoordinatorTests.cs" /> <Compile Include="Viewers\BreadcrumbBridgeCoordinatorSupersessionTests.cs" /> <Compile Include="Viewers\BreadcrumbBridgeCoordinatorProbabilityTests.cs" /> diff --git a/QuickFiler/Controllers/EfcDataModel.FilingStem.cs b/QuickFiler/Controllers/EfcDataModel.FilingStem.cs new file mode 100644 index 000000000..179512eec --- /dev/null +++ b/QuickFiler/Controllers/EfcDataModel.FilingStem.cs @@ -0,0 +1,16 @@ +using UtilitiesCS.OutlookObjects.Folder; + +namespace QuickFiler.Controllers +{ + internal partial class EfcDataModel + { + /// <summary> + /// Provides the #637 seam for the string filing overload; normalization lands in P4-T1. + /// </summary> + internal static string ToFilingStemOrVerbatim(string candidatePath, string archiveAncestor) + { + _ = ArchiveStemContract.TryMakeArchiveRelative(candidatePath, archiveAncestor, out _); + return candidatePath; + } + } +} diff --git a/QuickFiler/Controllers/EfcDataModel.cs b/QuickFiler/Controllers/EfcDataModel.cs index 00556faa9..c829f00f1 100644 --- a/QuickFiler/Controllers/EfcDataModel.cs +++ b/QuickFiler/Controllers/EfcDataModel.cs @@ -18,7 +18,7 @@ namespace QuickFiler.Controllers { - internal class EfcDataModel + internal partial class EfcDataModel { private static readonly log4net.ILog logger = log4net.LogManager.GetLogger( System.Reflection.MethodBase.GetCurrentMethod().DeclaringType @@ -334,7 +334,7 @@ bool moveConversation SaveMsg = saveEmail, SaveAttachments = attachments, SavePictures = savePictures, - DestinationOlStem = folderpath, + DestinationOlStem = ToFilingStemOrVerbatim(folderpath, olAncestor), Globals = Globals, OlAncestor = olAncestor, FsAncestorEquivalent = folderRoot, diff --git a/QuickFiler/QuickFiler.csproj b/QuickFiler/QuickFiler.csproj index 4c7b6edfe..09b52d19d 100644 --- a/QuickFiler/QuickFiler.csproj +++ b/QuickFiler/QuickFiler.csproj @@ -287,6 +287,7 @@ <ItemGroup> <Compile Include="Controllers\BayesianPerformanceController.cs" /> <Compile Include="Controllers\EfcDataModel.cs" /> + <Compile Include="Controllers\EfcDataModel.FilingStem.cs" /> <Compile Include="Controllers\BreadcrumbBridgeRouter.cs" /> <Compile Include="Controllers\BreadcrumbBridgeRouter.Arrows.cs" /> <Compile Include="Controllers\BreadcrumbBridgeRouter.Selection.cs" /> @@ -597,4 +598,4 @@ </ItemGroup> <Import Project="..\packages\System.Reactive.7.0.0\build\System.Reactive.targets" Condition="Exists('..\packages\System.Reactive.7.0.0\build\System.Reactive.targets')" /> <Import Project="..\packages\Microsoft.Web.WebView2.1.0.4129.50\build\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.4129.50\build\Microsoft.Web.WebView2.targets')" /> -</Project> \ No newline at end of file +</Project> diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t15-retry-1.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t15-retry-1.md new file mode 100644 index 000000000..73c8f7520 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t15-retry-1.md @@ -0,0 +1,21 @@ +Timestamp: 2026-08-31T10:26:19-04:00 +Command: pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p0-t15-baseline.cobertura.xml +EXIT_CODE: 0 + +#592 qualification evidence: +- The preserved first-attempt artifact records eight QuickFiler pump/dispatcher tests that each timed out after 60,000ms. +- The first attempt reported no completed totals and did not create the required Cobertura document. +- This retry used the unchanged canonical nine-assembly command and no runsettings, worker-count, timeout, filter, wrapper, or assembly-list changes. + +Pre-retry process counts: +- dotnet-coverage targeting this worktree: 0 +- vstest.console targeting this worktree: 0 +- testhost targeting this worktree: 0 +- MSBuild nodes remaining from P0-T13/P0-T14: 0 + +Machine-load observation: CPU load was 76%; 71,702 MB of 130,334 MB physical memory was free. The only observed dotnet process was the VS Code C# Dev Kit project-system build host and did not target this worktree. + +Output Summary: The retry discovered the canonical nine test assemblies and completed successfully: Total tests: 6876; Passed: 6876; Failed: 0; total time: 1.0340 minutes. It created coverage/p0-t15-baseline.cobertura.xml (10,767,247 bytes). Coverage attributes: line-rate=0.853428, branch-rate=0.793049, lines-covered=54808, lines-valid=64221, branches-covered=13052, branches-valid=16458. Derived line coverage: 85.3428%. Derived branch coverage: 79.3049%. + +BASELINE_FAILURE_SET: +- empty (the retry passed all tests). diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t16-coverage-headline.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t16-coverage-headline.md new file mode 100644 index 000000000..9be483fc2 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t16-coverage-headline.md @@ -0,0 +1,4 @@ +Timestamp: 2026-08-31T10:26:48-04:00 +Command: pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; $c = $d.SelectSingleNode("/coverage"); foreach ($a in @("line-rate","branch-rate","lines-covered","lines-valid","branches-covered","branches-valid")) { $a + "=" + $c.GetAttribute($a) }' +EXIT_CODE: 0 +Output Summary: line-rate=0.853428; branch-rate=0.793049; lines-covered=54808; lines-valid=64221; branches-covered=13052; branches-valid=16458. Derived baseline line coverage: 85.3428%. Derived baseline branch coverage: 79.3049%. All six attributes and both derived percentages match evidence/baseline/p0-t15-retry-1.md. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t17-baseline-uncovered-lines.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t17-baseline-uncovered-lines.md new file mode 100644 index 000000000..701666983 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p0-t17-baseline-uncovered-lines.md @@ -0,0 +1,7 @@ +Timestamp: 2026-08-31T10:27:25-04:00 +Command: pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; foreach ($f in @("QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcDataModel.cs")) { $u = @(); foreach ($c in $d.SelectNodes("//class")) { if ($c.GetAttribute("filename") -eq $f) { foreach ($l in $c.SelectNodes("./lines/line")) { if ([int]$l.GetAttribute("hits") -eq 0) { $u += [int]$l.GetAttribute("number") } } } }; $f + " uncovered=" + (($u | Sort-Object -Unique) -join ",") } ' +EXIT_CODE: 0 +Output Summary: Recorded one uncovered-line set for each baseline production file. +QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs uncovered=29,30,193 +QuickFiler\Controllers\EfcDataModel.cs uncovered=181,183,184,185,189,190,191,192,193,194,195,197,198,199,200,201,202,203,204,205,206,207,208,210,211,212,213,214,215,216,217,218,219,220,221,248,249,250,253,254,345,346,362,363,364,365,366,367,368,370,371,386,387,388,389,390,391,392,394,395,406,407,408,409,410,411,412,413,414,415,416,417,418,419,451,452,453,454,457,458,460,463,464,465,466,467,469,470,471,472,473,474,475,478,480,481 +QuickFiler\Controllers\EfcDataModel.FilingStem.cs baseline=absent diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t1-selection-family.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t1-selection-family.md new file mode 100644 index 000000000..25466135b --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t1-selection-family.md @@ -0,0 +1,18 @@ +Timestamp: 2026-08-31T10:28:12-04:00 +Search 1: rg -n "Select(Row|HierarchyPath)\\s*\\(" --glob "*.cs" QuickFiler/Controllers/BreadcrumbBridgeRouter.cs QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs +Search 2: rg -n "^\\s+(private void )?Select(Row|HierarchyPath)\\(" --glob "*.cs" . +EXIT_CODE: 0 for both searches +Output Summary: Both searches returned the identical set of 9 lines: 2 declarations and 7 call sites. + +Declarations: +- BreadcrumbBridgeRouter.Selection.cs:83 SelectRow +- BreadcrumbBridgeRouter.Selection.cs:109 SelectHierarchyPath + +Call sites: +- BreadcrumbBridgeRouter.Selection.cs:33 +- BreadcrumbBridgeRouter.Selection.cs:47 +- BreadcrumbBridgeRouter.cs:201 +- BreadcrumbBridgeRouter.cs:286 +- BreadcrumbBridgeRouter.Arrows.cs:138 +- BreadcrumbBridgeRouter.Arrows.cs:153 +- BreadcrumbBridgeRouter.Arrows.cs:161 diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t2-movetofolder-family.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t2-movetofolder-family.md new file mode 100644 index 000000000..bbf1846d3 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t2-movetofolder-family.md @@ -0,0 +1,21 @@ +Timestamp: 2026-08-31T10:28:53-04:00 +Search 1: rg -n "MoveToFolder" --glob "*.cs" . +Search 2: rg -n "MoveToFolderAsync\\s*\\(" --glob "*.cs" . +EXIT_CODE: 0 for both searches +Output Summary: Search 1 returned 23 lines across 6 files. Search 2 returned 10 lines across 5 files. Search 1 minus Search 2 yields 13 non-member textual references. + +Search 2 declarations (3): +- EfcDataModel.cs:303 +- EfcDataModel.cs:398 +- EfcHomeController.ExecuteMoves.cs:89 + +Search 2 call sites (7): +- EfcHomeController.ExecuteMoves.cs:78 +- EfcHomeController.ExecuteMoves.cs:98 +- EfcDataModel.cs:408 +- EfcFormController.cs:537 +- EfcFormController.cs:844 +- EfcHomeControllerExecuteMovesTests.cs:87 +- EfcDataModelArchiveRootTests.cs:314 + +Search 1 non-members (13): the delegate property, its assignments and invocations in EfcHomeController.ExecuteMoves.cs; test method names in EfcHomeControllerExecuteMovesTests.cs and EfcDataModelArchiveRootTests.cs; and the EfcHomeControllerTests.cs:55 comment. EfcHomeControllerTests.cs is the one stem-search-only file. EfcDataModelArchiveRootTests.cs appears in both searches and contributes call site :314. The prior 16-line research/spec figure predates issue #638. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t3-selectedfolderpath-surface.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t3-selectedfolderpath-surface.md new file mode 100644 index 000000000..2f4bc53c0 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t3-selectedfolderpath-surface.md @@ -0,0 +1,14 @@ +Timestamp: 2026-08-31T10:29:39-04:00 +Search 1: rg -c "SelectedFolderPath" --glob "*.cs" . +Search 2: rg -n "SelectedFolderPath" --glob "*.cs" QuickFiler/ UtilitiesCS/ TaskMaster/ ToDoModel/ Tags/ TaskVisualization/ +EXIT_CODE: 0 for both searches +Output Summary: Search 1 returned 74 lines across 9 files (3 production files and 6 test files). Search 2 returned 9 lines across exactly 3 production files. + +Production classification: +- Declaration: BreadcrumbBridgeRouter.cs:59 +- Documentation reference: BreadcrumbBridgeRouter.cs:61 +- Writes: BreadcrumbBridgeRouter.cs:145; BreadcrumbBridgeRouter.Selection.cs:134 +- Reads: BreadcrumbBridgeRouter.cs:143; BreadcrumbBridgeRouter.Selection.cs:138; EfcFormController.cs:321 +- Event-only: BreadcrumbBridgeRouter.cs:62; BreadcrumbBridgeRouter.cs:146 + +Search 1 per-file counts: EfcFormController.cs=1; BreadcrumbBridgeRouter.Selection.cs=2; BreadcrumbBridgeRouter.cs=6; BreadcrumbBridgeRouterTests.Selection.cs=12; BreadcrumbBridgeRouterTests.cs=2; BreadcrumbBridgeRouterQueueTests.Part2.cs=24; BreadcrumbBridgeRouterQueueTests.cs=4; BreadcrumbBridgeRouterIssue614Tests.cs=11; BreadcrumbBridgeRouterIssue439Tests.cs=12. The production/test split is 3/6, not the 2/7 in research section 7. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t4-deferral-records.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t4-deferral-records.md new file mode 100644 index 000000000..52e26e36e --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t4-deferral-records.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T10:30:11-04:00 +Search 1: rg -n "deferred to issue #637" --glob "*.cs" . +Search 2: rg -n "#637" --glob "*.cs" . +EXIT_CODE: 0 for both searches +Output Summary: Search 1 returned exactly 3 deferral records. Search 2 returned the same 3-line set; no additional #637 references required classification. + +Deferral records (verbatim): +- QuickFiler/Controllers/EfcSelectionGuard.cs:30: /// normalization in BreadcrumbBridgeRouter.SelectRow is deferred to issue #637. +- QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146: // RC-1 inversion: rooted values are never filing stems here; normalization is deferred to issue #637. +- QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:152: "a rooted value is never a filing stem at this surface and producer-side normalization is deferred to issue #637" diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t5-toarchiverelativestem-tests.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t5-toarchiverelativestem-tests.md new file mode 100644 index 000000000..6b29b3e1a --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t5-toarchiverelativestem-tests.md @@ -0,0 +1,5 @@ +Timestamp: 2026-08-31T10:30:44-04:00 +Search 1: rg -n "public void ToArchiveRelativeStem_" QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs +Search 2: rg -n "EfcDataModel\\.ToArchiveRelativeStem\\(" QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs +EXIT_CODE: 0 for both searches +Output Summary: Both searches returned exactly 8 lines. Declaration lines: 21, 34, 48, 62, 72, 87, 100, 111. ToArchiveRelativeStem_ArchiveRootItself_Throws is the declaration at line 62. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t6-passthrough-tests.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t6-passthrough-tests.md new file mode 100644 index 000000000..799f029f3 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t6-passthrough-tests.md @@ -0,0 +1,11 @@ +Timestamp: 2026-08-31T10:31:34-04:00 +Search 1: rg -n "Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection|SegmentActivate_WithNoBoundArchiveRoot_PreservesThePassThroughMode" --glob "*.cs" . +Search 2: rg -n "BindRowsAsync\\(" --glob "*.cs" QuickFiler.Test/Controllers/ +EXIT_CODE: 0 for both searches +Output Summary: Search 1 returned exactly 2 declarations. Search 2 found 21 BindRowsAsync sites; classification identified the same two pass-through cases and 19 calls that bind a non-empty archive root, are unrelated queue/selection coverage, or use a non-pass-through input. + +Pass-through cases: +- BreadcrumbBridgeRouterIssue439Tests.cs:619 Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection; binds @"\\" at :645 and asserts Be(@"\\Archive") at :665. +- BreadcrumbBridgeRouterIssue614Tests.cs:188 SegmentActivate_WithNoBoundArchiveRoot_PreservesThePassThroughMode; uses the three-argument overload at :213 and asserts Be(@"\\Archive") at :221. + +All other BindRowsAsync hits were classified as non-pass-through because they bind a non-empty root, bind row data for a separate test contract, or exercise the null-row overload; none asserts the no-bound-root full-hierarchy pass-through result. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t7-file-line-counts.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t7-file-line-counts.md new file mode 100644 index 000000000..6d69f9f59 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t7-file-line-counts.md @@ -0,0 +1,8 @@ +Timestamp: 2026-08-31T10:32:12-04:00 +Construction 1: pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcSelectionGuard.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs","QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }' +Construction 2: rg -c "^" --glob "*.cs" QuickFiler/Controllers/ QuickFiler.Test/Controllers/ filtered to the same six paths +Baseline absence command: pwsh -NoProfile -Command 'Test-Path "QuickFiler\Controllers\EfcDataModel.FilingStem.cs"' +EXIT_CODE: 0 for all commands +Output Summary: Both line-count constructions agree. EfcDataModel.cs=485; BreadcrumbBridgeRouter.Selection.cs=209; EfcSelectionGuard.cs=79; BreadcrumbBridgeRouterIssue439Tests.cs=694; EfcDataModelIssue614Tests.cs=123; EfcSelectionGuardTests.cs=296. EfcDataModel.FilingStem.cs baseline=False. + +spec.md:401 lists 424 for EfcDataModel.cs, while AC25 at spec.md:977 lists 485. The ecdb1c84 planning base had 423. The merged-tree value 485 governs, leaving 15 lines to the 500-line limit. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t8-pinning-assertion.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t8-pinning-assertion.md new file mode 100644 index 000000000..ac0acd998 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/baseline/p1-t8-pinning-assertion.md @@ -0,0 +1,7 @@ +Timestamp: 2026-08-31T10:33:02-04:00 +Construction 1: rg -n "SelectedFolderPath\\.Should\\(\\)\\.Be\\(" --glob "*.cs" QuickFiler.Test/ +Construction 2: rg -n "rowSelected|SelectFirstRow" --glob "*.cs" QuickFiler.Test/ +EXIT_CODE: 0 for both searches +Output Summary: Both constructions identify one assertion that must change: BreadcrumbBridgeRouterIssue439Tests.cs:165, `router.SelectedFolderPath.Should().Be(fullTarget);`, inside the method declared at line 119. It is the sole existing selected-row assertion where a presented filing target is a full Outlook path at or under a non-empty bound root. + +Construction 1 classifications: all other assertions are relative stems, no-root pass-through values, pseudo-row outcomes, unchanged state, or rows outside the bound-root/full-Outlook-path contract. Construction 2 classifications: the only matching non-empty-bound-root full-target row selection is the Issue439 test at :156→:165; the other rowSelected/SelectFirstRow hits are selection behavior, queue behavior, no-root behavior, documentation, invalid-row behavior, or already-relative targets. No test binds a presented row whose filing target equals the bound archive root; zero tests depend on archive-root-exact selection. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t10-issue638-preserved.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t10-issue638-preserved.md new file mode 100644 index 000000000..41ddc2af2 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t10-issue638-preserved.md @@ -0,0 +1,6 @@ +Timestamp: 2026-08-31T10:35:54-04:00 +Command: pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; $asm = Join-Path (Get-Location).Path "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll"; & $vstest $asm /InIsolation "/TestCaseFilter:FullyQualifiedName~EfcDataModelArchiveRootTests&TestCategory!=LiveOutlook" /Logger:trx "/ResultsDirectory:coverage\testresults\p2-t10"; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +Output Summary: The filter matched 11 tests; 11 passed and 0 failed. The output did not contain `No test matches the given testcase filter`. + +MoveToFolderAsync_WhenArchiveRootResolves_StillReadsItOnce: PASS. Its `Times.Once()` assertion proves P2-T4 passed the existing `olAncestor` local rather than reading `Globals.Ol.ArchiveRootPath` a second time. The failing set is empty, which is a subset of the prior baseline failure set. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t8-msbuild-analyzers.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t8-msbuild-analyzers.md new file mode 100644 index 000000000..9a03ecacf --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t8-msbuild-analyzers.md @@ -0,0 +1,4 @@ +Timestamp: 2026-08-31T10:34:53-04:00 +Command: pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +Output Summary: Build succeeded after the partial-class seam, project registration, and regression-test additions. MSBuild executed Rebuild target(s), reported 5 Warning(s), and 0 Error(s). diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t9-msbuild-nullable.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t9-msbuild-nullable.md new file mode 100644 index 000000000..973b0bfbe --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t9-msbuild-nullable.md @@ -0,0 +1,5 @@ +Timestamp: 2026-08-31T10:35:29-04:00 +Command: pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +NULLABLE_OPT_IN_PROPERTY: absent +Output Summary: Build succeeded with the required warnings-as-errors configuration. MSBuild executed Rebuild target(s), reported 5 Warning(s), and 0 Error(s). diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index 38edcfaf6..ac96991e7 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -3,9 +3,9 @@ - **Issue:** #637 - **Parent (optional):** none - **Owner:** drmoisan -- **Last Updated:** 2026-08-31T08-59 +- **Last Updated:** 2026-08-31T10-22 - **Status:** Ready for Codex preflight -- **Version:** 0.6 +- **Version:** 0.8 - **Work Mode:** full-bug (from `issue.md`); `spec.md` is the sole acceptance-criteria source (AC1-AC30). ## Conventions (read before executing any task) @@ -604,28 +604,39 @@ no downstream artifact inherits a wrong figure. non-zero, record the full diagnostic list under a section headed `BASELINE_BUILD_RED:`, stop, and report to the orchestrator; do not proceed to the next task and do not attempt a repair, because a pre-existing red baseline is outside this plan's scope. -- [ ] [P0-T15] Capture the baseline full test run with coverage. Run - `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p0-t15-baseline.cobertura.xml` - and write `evidence/baseline/p0-t15-mstest-coverage.md`. `coverage/*` is gitignored - (`.gitignore:144`), so the Cobertura document does not dirty the tree. Acceptance: the artifact - records `EXIT_CODE:`, the number of discovered test assemblies printed by the wrapper, the total - and passed and failed test counts, and a section headed `BASELINE_FAILURE_SET:` naming every - failing test's fully qualified name (empty when the run passes). The file - `coverage/p0-t15-baseline.cobertura.xml` exists after the run. `Output Summary:` additionally - carries the six numeric `/coverage` attribute values and the derived line and branch - percentages. As a read-only P0-T15 substep, run the P0-T16 coverage-headline command against - `coverage\p0-t15-baseline.cobertura.xml`, record those values in this P0-T15 `Output Summary:`, - verify P0-T15, and check P0-T15 off before starting P0-T16. P0-T16 does not modify P0-T15. -- [ ] [P0-T16] Read the baseline numeric coverage headline. Run +- [x] [P0-T15] Capture the baseline full test run with coverage. Preserve the existing failed + first-attempt evidence file `evidence/baseline/p0-t15-mstest-coverage.md` unchanged. Permit at + most two recovery retries, and only for the documented #592 60,000ms QuickFiler + pump/dispatcher-timeout cascade. Before each retry, verify that no `dotnet-coverage`, `vstest`, + or `testhost` process targeting this worktree remains; wait until the 17 P0-T13/P0-T14 MSBuild + nodes have exited; and write a separate immutable retry artifact + `evidence/baseline/p0-t15-retry-<attempt>.md` that records `Timestamp:`, the unchanged command, + `EXIT_CODE:`, #592 qualification evidence, pre-retry process counts, machine-load observation, + and `Output Summary:`. Execute this unchanged command in one persistent terminal session and + poll that same process ID to terminal completion; do not use `Start-Process` or another launcher: + `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p0-t15-baseline.cobertura.xml`. + Do not alter runsettings, worker count, timeout, filter, wrapper, or the canonical nine-assembly + list. `coverage/*` is gitignored (`.gitignore:144`), so the Cobertura document does not dirty the + tree. Acceptance: a retry exits `0`; reports the nine canonical assemblies and total, passed, and + failed test counts; creates `coverage/p0-t15-baseline.cobertura.xml`; and records all six numeric + `/coverage` attributes in its `Output Summary:` with derived line and branch percentages. The + successful retry artifact includes a `BASELINE_FAILURE_SET:` section naming every failing test's + fully qualified name (empty when the run passes). As a read-only P0-T15 substep, run the P0-T16 + coverage-headline command against `coverage\p0-t15-baseline.cobertura.xml`, record those values + in the successful retry artifact, verify P0-T15, and check P0-T15 off before starting P0-T16. + P0-T16 does not modify P0-T15 evidence. On a non-#592 failure, or after two cleared-load #592 + retry attempts without the required output, stop and report the baseline blocker; do not perform + further retries or any repair. +- [x] [P0-T16] Read the baseline numeric coverage headline. Run `pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; $c = $d.SelectSingleNode("/coverage"); foreach ($a in @("line-rate","branch-rate","lines-covered","lines-valid","branches-covered","branches-valid")) { $a + "=" + $c.GetAttribute($a) }'` and write `evidence/baseline/p0-t16-coverage-headline.md`. Acceptance: `EXIT_CODE: 0`, and `Output Summary:` records all six numeric values, plus the derived baseline line-coverage percentage computed as `line-rate` multiplied by 100 and the derived branch percentage computed as `branch-rate` multiplied by 100. Confirm that all six attributes and both derived - percentages equal the values already recorded in - `evidence/baseline/p0-t15-mstest-coverage.md`; P0-T16 does not modify that artifact or any - prior checklist state. These are the baseline figures the Phase 7 delta task compares against. -- [ ] [P0-T17] Record the baseline uncovered-line sets for the two production files this plan changes. + percentages equal the values already recorded in the successful + `evidence/baseline/p0-t15-retry-<attempt>.md` artifact; P0-T16 does not modify that artifact or + any prior checklist state. These are the baseline figures the Phase 7 delta task compares against. +- [x] [P0-T17] Record the baseline uncovered-line sets for the two production files this plan changes. Run `pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p0-t15-baseline.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; foreach ($f in @("QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcDataModel.cs")) { $u = @(); foreach ($c in $d.SelectNodes("//class")) { if ($c.GetAttribute("filename") -eq $f) { foreach ($l in $c.SelectNodes("./lines/line")) { if ([int]$l.GetAttribute("hits") -eq 0) { $u += [int]$l.GetAttribute("number") } } } }; $f + " uncovered=" + (($u | Sort-Object -Unique) -join ",") } '` and write `evidence/baseline/p0-t17-baseline-uncovered-lines.md`. Acceptance: `EXIT_CODE: 0`, and @@ -643,7 +654,7 @@ Every number in this phase is a number that `spec.md` already carries inside an criterion. Each task verifies it by a search over the full symbol family and cross-checks it with a second, independently constructed search. No number in this phase is verified by a single-pass grep. -- [ ] [P1-T1] Re-derive the selection family census (AC9: 2 declarations, 7 call sites). +- [x] [P1-T1] Re-derive the selection family census (AC9: 2 declarations, 7 call sites). Search 1, path-anchored: `rg -n "Select(Row|HierarchyPath)\s*\(" --glob "*.cs" QuickFiler/Controllers/BreadcrumbBridgeRouter.cs QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs`. Search 2, syntax-anchored and independently constructed on unqualified-invocation form rather than on file paths: `rg -n "^\s+(private void )?Select(Row|HierarchyPath)\(" --glob "*.cs" .` @@ -657,7 +668,7 @@ second, independently constructed search. No number in this phase is verified by (`BreadcrumbBridgeRouter.Selection.cs:83` and `:109`) and call sites total 7 (`BreadcrumbBridgeRouter.cs:201`, `:286`, `BreadcrumbBridgeRouter.Arrows.cs:138`, `:153`, `:161`, `BreadcrumbBridgeRouter.Selection.cs:33`, `:47`). -- [ ] [P1-T2] Re-derive the `MoveToFolder` family census (AC16: 3 declarations, 7 call sites). +- [x] [P1-T2] Re-derive the `MoveToFolder` family census (AC16: 3 declarations, 7 call sites). Search 1, family-stem: `rg -n "MoveToFolder" --glob "*.cs" .` — the bare stem catches any non-`Async` sibling or partially renamed overload that an `Async`-suffixed pattern would miss. Search 2, independently constructed on invocation and declaration syntax: @@ -677,7 +688,7 @@ second, independently constructed search. No number in this phase is verified by `EfcDataModelArchiveRootTests.cs` appears in both searches, contributing the `:314` call site, and that the 16-line figure in research section 6 and in `spec.md` describes the tree before issue #638 merged. -- [ ] [P1-T3] Re-derive the `SelectedFolderPath` surface (AC24: 9 lines across 3 production files, 2 +- [x] [P1-T3] Re-derive the `SelectedFolderPath` surface (AC24: 9 lines across 3 production files, 2 writes, 3 reads). Search 1: `rg -c "SelectedFolderPath" --glob "*.cs" .`, recording the per-file counts. Search 2, independently constructed by scoping to the production project directories up front rather than by subtracting the test projects from Search 1's table: @@ -691,7 +702,7 @@ second, independently constructed search. No number in this phase is verified by (`:143`, `BreadcrumbBridgeRouter.Selection.cs:138`, `EfcFormController.cs:321`) and 2 event-only lines (`:62`, `:146`); and it records that the production split is 3 production files and 6 test files, not the 2-and-7 stated in research section 7. -- [ ] [P1-T4] Re-derive the stale deferral record census (AC22: 3 records). +- [x] [P1-T4] Re-derive the stale deferral record census (AC22: 3 records). Search 1, on the deferral phrase: `rg -n "deferred to issue #637" --glob "*.cs" .`. Search 2, independently constructed on the issue reference alone so it cannot miss a differently worded deferral: `rg -n "#637" --glob "*.cs" .`. Write @@ -701,7 +712,7 @@ second, independently constructed search. No number in this phase is verified by superset whose every additional line is enumerated in the artifact and individually classified as not a deferral claim; and the artifact quotes the current text of all three Search 1 lines verbatim. -- [ ] [P1-T5] Re-derive the existing `ToArchiveRelativeStem` test count (AC15: 8 tests). +- [x] [P1-T5] Re-derive the existing `ToArchiveRelativeStem` test count (AC15: 8 tests). Search 1, on the method-name convention: `rg -n "public void ToArchiveRelativeStem_" QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`. Search 2, independently constructed on the call to the member under test rather than on test @@ -709,7 +720,7 @@ second, independently constructed search. No number in this phase is verified by Write `evidence/baseline/p1-t5-toarchiverelativestem-tests.md`. Acceptance: both searches return exactly 8 lines; the artifact records the declaration line numbers 21, 34, 48, 62, 72, 87, 100, 111 and records that `ToArchiveRelativeStem_ArchiveRootItself_Throws` is the method at line 62. -- [ ] [P1-T6] Re-derive the no-bound-root pass-through test pair (AC4: 2 tests). +- [x] [P1-T6] Re-derive the no-bound-root pass-through test pair (AC4: 2 tests). Search 1, by name: `rg -n "Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection|SegmentActivate_WithNoBoundArchiveRoot_PreservesThePassThroughMode" --glob "*.cs" .`. Search 2, independently constructed on the binding mechanism that produces an empty bound root — @@ -722,7 +733,7 @@ second, independently constructed search. No number in this phase is verified by `:665`, and the second uses the three-argument overload at `:213` and asserts `Be(@"\Archive")` at `:221`; and Search 2's classification identifies the same two tests as the only pass-through cases and no others. -- [ ] [P1-T7] Re-derive the file line counts AC25 depends on. Construction 1: +- [x] [P1-T7] Re-derive the file line counts AC25 depends on. Construction 1: `pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcSelectionGuard.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs","QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }'`. Construction 2, independently constructed with a line-oriented search rather than a file read: `rg -c "^" --glob "*.cs" QuickFiler/Controllers/ QuickFiler.Test/Controllers/` filtered to the @@ -741,7 +752,7 @@ second, independently constructed search. No number in this phase is verified by `spec.md:401` states 424 for `EfcDataModel.cs` while AC25 at `spec.md:977` already states 485, that the `ecdb1c84` planning base had 423, and that the merged tree value 485 governs, leaving 15 lines of headroom to the 500-line limit. -- [ ] [P1-T8] Re-derive the single pinning assertion (AC20: exactly 1 existing assertion changes). +- [x] [P1-T8] Re-derive the single pinning assertion (AC20: exactly 1 existing assertion changes). Construction 1, on the assertion form: `rg -n "SelectedFolderPath\.Should\(\)\.Be\(" --glob "*.cs" QuickFiler.Test/` with every hit classified by whether the selected row's filing target is a full Outlook path at or under a @@ -769,7 +780,7 @@ than left to Phase 4: the `partial` keyword on the existing declaration (P2-T1), (P2-T2), and the project registration that makes the new file compile (P2-T3). Only then does P2-T4 redirect the assignment. Reversing any of those orders produces a build that does not compile. -- [ ] [P2-T1] Make the existing declaration partial. In `QuickFiler/Controllers/EfcDataModel.cs`, +- [x] [P2-T1] Make the existing declaration partial. In `QuickFiler/Controllers/EfcDataModel.cs`, replace line 21, ` internal class EfcDataModel`, with ` internal partial class EfcDataModel`. This is a one-token, single-line substitution: the file stays at 485 lines and no line number in it shifts, which is what lets every line citation @@ -784,7 +795,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe lines; and `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.cs").Count'` reports exactly 485. -- [ ] [P2-T2] Create the new partial-class file +- [x] [P2-T2] Create the new partial-class file `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`. It contains exactly one `using` directive, `using UtilitiesCS.OutlookObjects.Folder;`, the namespace `QuickFiler.Controllers`, the type declaration written verbatim as ` internal partial class EfcDataModel`, and one member: the @@ -817,7 +828,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe returns exactly 1 line; and `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.FilingStem.cs").Count'` is at most 500. -- [ ] [P2-T3] Register the new production file in the non-SDK project. Insert +- [x] [P2-T3] Register the new production file in the non-SDK project. Insert ` <Compile Include="Controllers\EfcDataModel.FilingStem.cs" />` into `QuickFiler/QuickFiler.csproj` immediately after the existing line 289, ` <Compile Include="Controllers\EfcDataModel.cs" />`, which sits inside the `ItemGroup` that @@ -831,7 +842,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe `rg -F -n 'Controllers\EfcDataModel.FilingStem.cs' QuickFiler/QuickFiler.csproj` returns exactly 1 line; that line is line 290; and it is inside the same `ItemGroup` that begins at line 287. -- [ ] [P2-T4] Redirect the `DestinationOlStem` assignment to the seam. In +- [x] [P2-T4] Redirect the `DestinationOlStem` assignment to the seam. In `QuickFiler/Controllers/EfcDataModel.cs`, change line 337 from ` DestinationOlStem = folderpath,` to ` DestinationOlStem = ToFilingStemOrVerbatim(folderpath, olAncestor),`. @@ -854,7 +865,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe line and it is line 284; and `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.cs").Count'` reports exactly 485. -- [ ] [P2-T5] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` containing +- [x] [P2-T5] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` containing the class `BreadcrumbBridgeRouterIssue637Tests` with exactly the ten test methods named in the "Fixed identifiers" section. Use fixture Shape 2 and do not invent a new fixture shape: a `[TestInitialize]` `Setup` and `[TestCleanup]` `Cleanup` modelled on @@ -875,7 +886,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe `rg -n "public void " QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`; and `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs").Count` is at most 500. -- [ ] [P2-T6] Register the new test file in the non-SDK project. Insert +- [x] [P2-T6] Register the new test file in the non-SDK project. Insert ` <Compile Include="Controllers\BreadcrumbBridgeRouterIssue637Tests.cs" />` into `QuickFiler.Test/QuickFiler.Test.csproj` immediately after the existing line 64, ` <Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.cs" />`. A file absent @@ -887,7 +898,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe regex engine can consume it. Acceptance: `rg -F -n 'Controllers\BreadcrumbBridgeRouterIssue637Tests.cs' QuickFiler.Test/QuickFiler.Test.csproj` returns exactly 1 line, and that line is inside the same `ItemGroup` that begins at line 57. -- [ ] [P2-T7] Add the change-B helper tests. In the existing file +- [x] [P2-T7] Add the change-B helper tests. In the existing file `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, add a new sibling `[TestClass]` `EfcDataModelIssue637Tests` containing exactly the eight test methods named in the "Fixed identifiers" section, reaching `EfcDataModel.ToFilingStemOrVerbatim` through the existing @@ -909,18 +920,18 @@ redirect the assignment. Reversing any of those orders produces a build that doe shows zero removed content lines, meaning zero lines beginning with a single `-`; and `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs").Count` is at most 500. -- [ ] [P2-T8] Run the analyzer build and write `evidence/regression-testing/p2-t8-msbuild-analyzers.md` +- [x] [P2-T8] Run the analyzer build and write `evidence/regression-testing/p2-t8-msbuild-analyzers.md` using the P0-T13 command verbatim. Acceptance: `EXIT_CODE: 0`; the output contains `(Rebuild target(s))`; and the `Error(s)` count is 0. A non-zero exit here means the seam, the partial-class split, the project registrations, or the new test files do not compile and must be repaired before P2-T10 runs. -- [ ] [P2-T9] Run the nullable build and write `evidence/regression-testing/p2-t9-msbuild-nullable.md` +- [x] [P2-T9] Run the nullable build and write `evidence/regression-testing/p2-t9-msbuild-nullable.md` using the P0-T14 command verbatim. Acceptance: `EXIT_CODE: 0`; the output contains `(Rebuild target(s))`; the recorded `Command:` line does not contain the solution-wide nullable opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. This gate is where an unused-parameter or nullable diagnostic introduced by the seam would surface as an error. -- [ ] [P2-T10] Prove the seam did not regress issue #638's single-read guarantee. Use the scoped +- [x] [P2-T10] Prove the seam did not regress issue #638's single-read guarantee. Use the scoped vstest command stated in full in P2-T11 below, with the filter `"/TestCaseFilter:FullyQualifiedName~EfcDataModelArchiveRootTests&TestCategory!=LiveOutlook"` and the results directory `coverage\testresults\p2-t10`, and write From 60e8df5f2a13b6e98d1b1511a7d275ddfce7d9c5 Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 10:52:37 -0400 Subject: [PATCH 09/16] fix(quickfiler): normalize rooted breadcrumb selections - Convert eligible bound-root targets to archive-relative filing stems - Correct #439 selection expectations while preserving provider lookup paths - Align selection guard documentation with producer-side normalization Refs: #637, #439 --- .../BreadcrumbBridgeRouterIssue439Tests.cs | 8 +-- .../Controllers/EfcSelectionGuardTests.cs | 4 +- .../BreadcrumbBridgeRouter.Selection.cs | 19 +++++-- .../Controllers/EfcDataModel.FilingStem.cs | 17 +++++- QuickFiler/Controllers/EfcSelectionGuard.cs | 2 +- .../other/p6-t5-evidence-redaction.md | 13 +++++ .../evidence/other/p6-t6-commit.md | 4 ++ .../p2-t11-router-tests-red.md | 21 ++++++++ .../p2-t12-helper-tests-red.md | 12 +++++ .../p2-t13-compile-include-observed.md | 19 +++++++ .../regression-testing/p3-t2-nesting.md | 43 +++++++++++++++ .../p3-t3-selectionfile-diff.md | 10 ++++ .../regression-testing/p3-t4-builds.md | 7 +++ .../p3-t5-router-tests-green.md | 12 +++++ .../p3-t6-router-siblings.md | 6 +++ .../p4-t1-helper-implemented.md | 10 ++++ .../regression-testing/p4-t2-helper-shape.md | 6 +++ .../regression-testing/p4-t3-builds.md | 7 +++ .../p4-t4-helper-tests-green.md | 6 +++ .../p4-t5-toarchiverelativestem-unchanged.md | 4 ++ .../p4-t6-nongoals-untouched.md | 11 ++++ .../p5-t4-provider-assertion-preserved.md | 11 ++++ .../p5-t5-single-assertion-change.md | 17 ++++++ .../p5-t6-issue439-green.md | 7 +++ .../p6-t4-deferral-cleared.md | 11 ++++ .../plan.2026-08-29T12-20.md | 54 +++++++++---------- 26 files changed, 300 insertions(+), 41 deletions(-) create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p6-t5-evidence-redaction.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p6-t6-commit.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t11-router-tests-red.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t12-helper-tests-red.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t13-compile-include-observed.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t2-nesting.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t3-selectionfile-diff.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t4-builds.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t5-router-tests-green.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t6-router-siblings.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t1-helper-implemented.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t2-helper-shape.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t3-builds.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-green.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t6-nongoals-untouched.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t4-provider-assertion-preserved.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t5-single-assertion-change.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t6-issue439-green.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p6-t4-deferral-cleared.md diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs index 74fbfd5d9..3419dab42 100644 --- a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs @@ -116,10 +116,10 @@ public void Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProba } [TestMethod] - public void Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch() + public void Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively() { - // Arrange: the presented target is already rooted with casing different from the - // configured root, so the provider must receive the original full path unchanged. + // Arrange: the presented target is rooted with casing different from the configured + // root, so the provider must receive the original full path unchanged (#439). const string archiveRoot = @"\Archive"; const string fullTarget = @"\aRcHiVe\Clients\North"; var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); @@ -162,7 +162,7 @@ public void Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiv p => p.ResolveLeafKeyAsync(fullTarget, It.IsAny<CancellationToken>()), Times.Once ); - router.SelectedFolderPath.Should().Be(fullTarget); + router.SelectedFolderPath.Should().Be(@"Clients\North"); } [TestMethod] diff --git a/QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs b/QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs index 03a4b1356..f6a331143 100644 --- a/QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs +++ b/QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs @@ -143,13 +143,13 @@ public void IsValidFilingSelection_SeparatorBoundaryNearMiss_IsRejected() [TestMethod] public void IsValidFilingSelection_RootedTargetUnderArchiveRoot_IsRejected() { - // RC-1 inversion: rooted values are never filing stems here; normalization is deferred to issue #637. + // RC-1 inversion: rooted values are never filing stems here; the producer normalizes first. // Arrange / Act / Assert EfcSelectionGuard .IsValidFilingSelection(@"\aRcHiVe\Clients\North") .Should() .BeFalse( - "a rooted value is never a filing stem at this surface and producer-side normalization is deferred to issue #637" + "a rooted value is never a filing stem at this surface because the producer now normalizes before this predicate is reached" ); } diff --git a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs index b59270055..5b17a89fc 100644 --- a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs +++ b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs @@ -91,16 +91,25 @@ private void SelectRow(BreadcrumbRow row) row.Kind == BreadcrumbRowKind.TrashPseudoRow ? BreadcrumbRowBuilder.TrashRowText : row.FilingTarget; - // #614 D2: reject only an out-of-root FULL Outlook target; a rooted target at or - // under the root passes verbatim (#439) and no bound root leaves the row unguarded. + // #614 D2: normalize eligible rooted targets, preserving no-bound-root pass-through. if ( _boundRoot.Length != 0 && ArchiveStemContract.IsFullOutlookPath(selection) - && !ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out _) ) { - log.Error("Breadcrumb row rejected: target is outside the archive root."); - return; + if (!ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out string stem)) + { + log.Error("Breadcrumb row rejected: target is outside the archive root."); + return; + } + + if (stem.Length == 0) + { + log.Error("Breadcrumb row rejected: target is the archive root itself."); + return; + } + + selection = stem; } CommitSelection(row, selection); diff --git a/QuickFiler/Controllers/EfcDataModel.FilingStem.cs b/QuickFiler/Controllers/EfcDataModel.FilingStem.cs index 179512eec..97a67c3f3 100644 --- a/QuickFiler/Controllers/EfcDataModel.FilingStem.cs +++ b/QuickFiler/Controllers/EfcDataModel.FilingStem.cs @@ -5,11 +5,24 @@ namespace QuickFiler.Controllers internal partial class EfcDataModel { /// <summary> - /// Provides the #637 seam for the string filing overload; normalization lands in P4-T1. + /// Returns its input unchanged unless the input is a full Outlook path under the archive ancestor + /// supplied by the caller. Returns a value for every input and propagates no exception. /// </summary> internal static string ToFilingStemOrVerbatim(string candidatePath, string archiveAncestor) { - _ = ArchiveStemContract.TryMakeArchiveRelative(candidatePath, archiveAncestor, out _); + if ( + ArchiveStemContract.IsFullOutlookPath(candidatePath) + && ArchiveStemContract.TryMakeArchiveRelative( + candidatePath, + archiveAncestor, + out string stem + ) + && stem.Length != 0 + ) + { + return stem; + } + return candidatePath; } } diff --git a/QuickFiler/Controllers/EfcSelectionGuard.cs b/QuickFiler/Controllers/EfcSelectionGuard.cs index 3a9d693c4..26b0ec276 100644 --- a/QuickFiler/Controllers/EfcSelectionGuard.cs +++ b/QuickFiler/Controllers/EfcSelectionGuard.cs @@ -27,7 +27,7 @@ internal static class EfcSelectionGuard /// Rejects null, empty, and whitespace-only values; banner sentinels; and every full /// (rooted) path. Rooted values are rejected as such at this surface so the predicate /// agrees with <see cref="ArchiveStemContract.RequireArchiveRelativeStem"/>. Producer-side - /// normalization in BreadcrumbBridgeRouter.SelectRow is deferred to issue #637. + /// normalization in BreadcrumbBridgeRouter.SelectRow is implemented by issue #637. /// </para> /// <para> /// The filing predicate carries NO minimum-length rule (CR-1). Filing into an existing diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p6-t5-evidence-redaction.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p6-t5-evidence-redaction.md new file mode 100644 index 000000000..d0ce8c8bd --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p6-t5-evidence-redaction.md @@ -0,0 +1,13 @@ +Timestamp: 2026-08-31T10:51:19-04:00 +Command (search 1): `rg -F -n 'C:\Users' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**' --glob '!**/p6-t5-evidence-redaction.md'` +ExpectedExitCode (search 1): 1 +Command (search 2): `rg -F -n '.trx' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**' --glob '!**/p6-t5-evidence-redaction.md'` +ExpectedExitCode (search 2): 1 +Output Summary: Both searches returned zero matches with exit 1. + +Rewritten evidence files: + +- `evidence/baseline/p0-t15-wrapper.stdout.log`: 2 absolute worktree-path replacements. +- `evidence/baseline/p0-t15-wrapper.stderr.log`: 1 absolute worktree-path replacement. + +A zero-match result over the two-component `C:\Users` prefix proves the account segment is absent as well, because on Windows that segment always follows the prefix immediately. Search 2 covers the vstest results filename, which carries account and machine identity without a preceding profile path and is therefore not covered by search 1. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p6-t6-commit.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p6-t6-commit.md new file mode 100644 index 000000000..6828e146d --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p6-t6-commit.md @@ -0,0 +1,4 @@ +Timestamp: 2026-08-31T10:51:19-04:00 +Signal: `PROGRESS_COMMIT_REQUIRED: P2-T11..P6-T6` +Scope: `QuickFiler`, `QuickFiler.Test`, and the active feature folder. +Status: Awaiting orchestrator-owned canonical commit-context collection, routed commit-steward message generation, commit creation, and checkpoint SHA persistence. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t11-router-tests-red.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t11-router-tests-red.md new file mode 100644 index 000000000..7d549348f --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t11-router-tests-red.md @@ -0,0 +1,21 @@ +Timestamp: 2026-08-31T10:41:32-04:00 +Command: `pwsh -NoProfile -Command '<resolved vstest command> /TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue637Tests&TestCategory!=LiveOutlook /Logger:trx /ResultsDirectory:coverage\\testresults\\p2-t11'` +ExpectedExitCode: 1 +EXIT_CODE: 1 +Output Summary: 10 tests executed; 5 passed and 5 failed. No "No test matches the given testcase filter" output occurred. + +Expected failing tests: + +- `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected` +- `RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection` +- `RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` +- `RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem` +- `SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` + +Expected passing tests: + +- `RowSelected_RelativeFilingTarget_CommitsTheValueVerbatim` +- `RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim` +- `RowSelected_OutOfRootRootedTarget_IsStillRejected` +- `RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected` +- `RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t12-helper-tests-red.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t12-helper-tests-red.md new file mode 100644 index 000000000..5e394f8c3 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t12-helper-tests-red.md @@ -0,0 +1,12 @@ +Timestamp: 2026-08-31T10:41:32-04:00 +Command: `pwsh -NoProfile -Command '<resolved vstest command> /TestCaseFilter:FullyQualifiedName~EfcDataModelIssue637Tests&TestCategory!=LiveOutlook /Logger:trx /ResultsDirectory:coverage\\testresults\\p2-t12'` +ExpectedExitCode: 1 +EXIT_CODE: 1 +Output Summary: 8 tests executed; 6 passed and 2 failed. No "No test matches the given testcase filter" output occurred. + +Expected failing tests: + +- `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` +- `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem` + +The other six helper tests passed, confirming that the seam already returns the candidate input verbatim for non-normalizable cases. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t13-compile-include-observed.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t13-compile-include-observed.md new file mode 100644 index 000000000..6a3a012ac --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p2-t13-compile-include-observed.md @@ -0,0 +1,19 @@ +Timestamp: 2026-08-31T10:41:32-04:00 +Command: Extract `UnitTestResult` attributes from `<trx-file>` after the P2-T11 run. +EXIT_CODE: 0 +Output Summary: Exactly 10 fixed-method results were observed. The explicit compile item is present; removing it would reduce the observed count to 0 in this non-SDK project. + +Compile Include: `<Compile Include="Controllers\BreadcrumbBridgeRouterIssue637Tests.cs" />` + +Observed result attributes: + +- `testName="RowSelected_OutOfRootRootedTarget_IsStillRejected" outcome="Passed"` +- `testName="RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim" outcome="Passed"` +- `testName="RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem" outcome="Failed"` +- `testName="RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim" outcome="Passed"` +- `testName="RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem" outcome="Failed"` +- `testName="RowSelected_RelativeFilingTarget_CommitsTheValueVerbatim" outcome="Passed"` +- `testName="SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem" outcome="Failed"` +- `testName="RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection" outcome="Failed"` +- `testName="RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected" outcome="Passed"` +- `testName="RowSelected_ArchiveRootExactFilingTarget_IsNotSelected" outcome="Failed"` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t2-nesting.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t2-nesting.md new file mode 100644 index 000000000..566dc21aa --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t2-nesting.md @@ -0,0 +1,43 @@ +Timestamp: 2026-08-31T10:42:17-04:00 +Command: Scoped `rg` verification of SelectRow diagnostics and guard structure. +EXIT_CODE: 0 +Output Summary: Each diagnostic literal occurs once, neither contains `@`, and the first conjunction remains `_boundRoot.Length != 0`. + +```csharp +private void SelectRow(BreadcrumbRow row) +{ + if (row.Kind == BreadcrumbRowKind.Banner) + { + return; // Banner rows are never selectable. + } + + string selection = + row.Kind == BreadcrumbRowKind.TrashPseudoRow + ? BreadcrumbRowBuilder.TrashRowText + : row.FilingTarget; + // #614 D2: normalize eligible rooted targets, preserving no-bound-root pass-through. + if ( + _boundRoot.Length != 0 + && ArchiveStemContract.IsFullOutlookPath(selection) + ) + { + if (!ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out string stem)) + { + log.Error("Breadcrumb row rejected: target is outside the archive root."); + return; + } + + if (stem.Length == 0) + { + log.Error("Breadcrumb row rejected: target is the archive root itself."); + return; + } + + selection = stem; + } + + CommitSelection(row, selection); +} +``` + +The `_boundRoot.Length != 0` condition remains the first conjunct, preserving no-bound-root pass-through mode. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t3-selectionfile-diff.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t3-selectionfile-diff.md new file mode 100644 index 000000000..13d56aabd --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t3-selectionfile-diff.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T10:42:17-04:00 +Command: `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` +EXIT_CODE: 0 +Output Summary: All edited hunks are within original lines 94-102, hence within the allowed original lines 83-107. No hunk addresses original lines 109-139. + +Hunk headers: + +- `@@ -94,2 +94 @@` +- `@@ -99 +97,0 @@` +- `@@ -102,2 +100,13 @@` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t4-builds.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t4-builds.md new file mode 100644 index 000000000..88324ad08 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t4-builds.md @@ -0,0 +1,7 @@ +Timestamp: 2026-08-31T10:43:37-04:00 +Command (analyzers): `pwsh -NoProfile -Command '<resolved MSBuild> TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true'` +EXIT_CODE (analyzers): 0 +Command (nullable): `pwsh -NoProfile -Command '<resolved MSBuild> TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true'` +EXIT_CODE (nullable): 0 +NULLABLE_OPT_IN_PROPERTY: absent +Output Summary: Both builds succeeded with `(Rebuild target(s))` observed. Each reported 5 warnings and 0 errors. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t5-router-tests-green.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t5-router-tests-green.md new file mode 100644 index 000000000..971f803f0 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t5-router-tests-green.md @@ -0,0 +1,12 @@ +Timestamp: 2026-08-31T10:43:55-04:00 +Command: `pwsh -NoProfile -Command '<resolved vstest command> /TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue637Tests&TestCategory!=LiveOutlook /Logger:trx /ResultsDirectory:coverage\\testresults\\p3-t5'` +EXIT_CODE: 0 +Output Summary: 10 tests total; 10 passed; 0 failed; 0 skipped. + +The five former failures now pass: + +- `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected` +- `RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection` +- `RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` +- `RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem` +- `SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t6-router-siblings.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t6-router-siblings.md new file mode 100644 index 000000000..43a81143a --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p3-t6-router-siblings.md @@ -0,0 +1,6 @@ +Timestamp: 2026-08-31T10:43:55-04:00 +Command: `pwsh -NoProfile -Command '<resolved vstest command> /TestCaseFilter:(FullyQualifiedName~BreadcrumbBridgeRouterIssue614Tests|FullyQualifiedName~BreadcrumbBridgeRouterTests|FullyQualifiedName~BreadcrumbBridgeRouterQueueTests)&TestCategory!=LiveOutlook /Logger:trx /ResultsDirectory:coverage\\testresults\\p3-t6'` +EXIT_CODE: 0 +Output Summary: 58 tests executed; 58 passed; 0 failed. + +`RowSelected_OutOfRootFilingTarget_DoesNotStoreAFullOutlookPath` and `SegmentActivate_WithNoBoundArchiveRoot_PreservesThePassThroughMode` both passed. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t1-helper-implemented.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t1-helper-implemented.md new file mode 100644 index 000000000..6ee59c7b7 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t1-helper-implemented.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T10:44:37-04:00 +Command: Scoped source searches and line-count commands after the P4-T1 helper implementation. +EXIT_CODE: 0 +Output Summary: The helper has one full-path predicate use, no global reference, no `throw` token, 30 lines, and `EfcDataModel.cs` remains exactly 485 lines. + +Pre-edit `rg -n "throw" QuickFiler/Controllers/EfcDataModel.FilingStem.cs`: `ExpectedExitCode: 1`; zero matches. + +Post-edit `rg -n "throw" QuickFiler/Controllers/EfcDataModel.FilingStem.cs`: `ExpectedExitCode: 1`; zero matches. + +`rg -n "IsFullOutlookPath" QuickFiler/Controllers/EfcDataModel.FilingStem.cs` returned exactly line 15 inside the helper. `rg -n "Globals" QuickFiler/Controllers/EfcDataModel.FilingStem.cs` returned zero matches with `ExpectedExitCode: 1`. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t2-helper-shape.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t2-helper-shape.md new file mode 100644 index 000000000..65b8a6b5c --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t2-helper-shape.md @@ -0,0 +1,6 @@ +Timestamp: 2026-08-31T10:47:24-04:00 +Command: Scoped source inspection and `rg -n "ToFilingStemOrVerbatim" --glob "*.cs" QuickFiler/`. +EXIT_CODE: 0 +Output Summary: The helper declaration body spans lines 11-27 in `EfcDataModel.FilingStem.cs`; no helper declaration appears in `EfcDataModel.cs`. Its body has no `await`, `Globals`, `logger`, or `throw` token. + +`QuickFiler/` has exactly one declaration at `EfcDataModel.FilingStem.cs:11` and exactly one production call at `EfcDataModel.cs:337`. The remaining references are the eight tests in `EfcDataModelIssue637Tests`; no additional production call exists. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t3-builds.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t3-builds.md new file mode 100644 index 000000000..2c6a68f62 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t3-builds.md @@ -0,0 +1,7 @@ +Timestamp: 2026-08-31T10:46:55-04:00 +Command (analyzers): `pwsh -NoProfile -Command '<resolved MSBuild> TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true'` +EXIT_CODE (analyzers): 0 +Command (nullable): `pwsh -NoProfile -Command '<resolved MSBuild> TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true'` +EXIT_CODE (nullable): 0 +NULLABLE_OPT_IN_PROPERTY: absent +Output Summary: Both Rebuild invocations completed successfully after P4-T1. The rebuilt `QuickFiler.Test.dll` is present with the P4-T1 changes. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-green.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-green.md new file mode 100644 index 000000000..ca7b60867 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-green.md @@ -0,0 +1,6 @@ +Timestamp: 2026-08-31T10:47:13-04:00 +Command: `pwsh -NoProfile -Command '<resolved vstest command> /TestCaseFilter:FullyQualifiedName~EfcDataModelIssue637Tests&TestCategory!=LiveOutlook /Logger:trx /ResultsDirectory:coverage\\testresults\\p4-t4'` +EXIT_CODE: 0 +Output Summary: 8 tests total; 8 passed; 0 failed. + +`ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` and `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem` now pass. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md new file mode 100644 index 000000000..a4a517bc5 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md @@ -0,0 +1,4 @@ +Timestamp: 2026-08-31T10:47:13-04:00 +Command: Scoped Issue614 vstest run and cached diff against `0eda184ca0009bc79ac9b7146897270c17c095fa`. +EXIT_CODE: 0 +Output Summary: 8 `EfcDataModelIssue614Tests` passed, including `ToArchiveRelativeStem_ArchiveRootItself_Throws`. The cached diff contains added Issue637 test content only and zero removed content lines. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t6-nongoals-untouched.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t6-nongoals-untouched.md new file mode 100644 index 000000000..aacd128b0 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t6-nongoals-untouched.md @@ -0,0 +1,11 @@ +Timestamp: 2026-08-31T10:47:24-04:00 +Command: `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -U0 -- QuickFiler/Controllers/EfcDataModel.cs` +EXIT_CODE: 0 +Output Summary: Exactly two single-line hunks were found; protected ranges were not touched. + +Hunk headers: + +- `@@ -21 +21 @@` +- `@@ -337 +337 @@` + +The only `Globals.Ol.ArchiveRootPath` read remains at line 284, inside `TryGetArchiveRoot`'s `try` block at lines 282-286. The `catch (InvalidOperationException ex)` remains at line 287. No read was added, removed, or moved. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t4-provider-assertion-preserved.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t4-provider-assertion-preserved.md new file mode 100644 index 000000000..88a8a4bc7 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t4-provider-assertion-preserved.md @@ -0,0 +1,11 @@ +Timestamp: 2026-08-31T10:48:27-04:00 +Command: Cached base diff of `BreadcrumbBridgeRouter.cs` and source inspection of the provider verification. +EXIT_CODE: 0 +Output Summary: `BreadcrumbBridgeRouter.cs` has no cached diff. The companion `ResolveLeafKeyAsync(fullTarget, ...)` verification remains unchanged. + +```csharp +provider.Verify( + p => p.ResolveLeafKeyAsync(fullTarget, It.IsAny<CancellationToken>()), + Times.Once +); +``` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t5-single-assertion-change.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t5-single-assertion-change.md new file mode 100644 index 000000000..69424f2e8 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t5-single-assertion-change.md @@ -0,0 +1,17 @@ +Timestamp: 2026-08-31T10:48:27-04:00 +Command: Line-count and cached QuickFiler.Test diff inspection against `0eda184ca0009bc79ac9b7146897270c17c095fa`. +EXIT_CODE: 0 +Output Summary: The issue #439 test file remains 694 lines. Exactly one removed assertion matches `.Should()`: `router.SelectedFolderPath.Should().Be(fullTarget);`. + +This is a deliberate spec correction: the issue #439 criterion that a rooted target survives selection is superseded by issue #614's archive-relative-stem invariant, which #614 enforced on the `SelectHierarchyPath` half and at the filing boundary but not on the `SelectRow` half. This is not a weakened test. + +Removed method name: `Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch` + +Added method name: `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` + +Replacement comments: + +```csharp +// Arrange: the presented target is rooted with casing different from the configured +// root, so the provider must receive the original full path unchanged (#439). +``` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t6-issue439-green.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t6-issue439-green.md new file mode 100644 index 000000000..254fd8161 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p5-t6-issue439-green.md @@ -0,0 +1,7 @@ +Timestamp: 2026-08-31T10:49:04-04:00 +Command (build): `pwsh -NoProfile -Command '<resolved MSBuild> TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true'` +Build EXIT_CODE: 0 +Build output: `(Rebuild target(s))` observed. +Command (tests): `pwsh -NoProfile -Command '<resolved vstest command> /TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue439Tests&TestCategory!=LiveOutlook /Logger:trx /ResultsDirectory:coverage\\testresults\\p5-t6'` +EXIT_CODE: 0 +Output Summary: 10 tests passed; 0 failed. `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` and `Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection` both passed. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p6-t4-deferral-cleared.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p6-t4-deferral-cleared.md new file mode 100644 index 000000000..3a358ac87 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p6-t4-deferral-cleared.md @@ -0,0 +1,11 @@ +Timestamp: 2026-08-31T10:50:39-04:00 +Command (build): `pwsh -NoProfile -Command '<resolved MSBuild> TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true'` +Build EXIT_CODE: 0 +Build output: `(Rebuild target(s))` observed. +Command (deferral search): `rg -c "deferred to issue #637" --glob "*.cs" .` +ExpectedExitCode (deferral search): 1 +Command (tests): `pwsh -NoProfile -Command '<resolved vstest command> /TestCaseFilter:FullyQualifiedName~EfcSelectionGuardTests&TestCategory!=LiveOutlook /Logger:trx /ResultsDirectory:coverage\\testresults\\p6-t4'` +EXIT_CODE: 0 +Output Summary: The deferral search returned zero matches with exit 1. The scoped guard suite ran 25 tests, 25 passed, 0 failed. `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` passed. + +The cached `EfcSelectionGuard.cs` diff changes only the documentation line 30. Neither `IsValidFilingSelection` nor `IsValidCreationSelection` has an executable line change. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index ac96991e7..5cdffb60b 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -957,7 +957,7 @@ redirect the assignment. Reversing any of those orders produces a build that doe canonical commit context and the routed commit-steward profile, then recorded the completed interval's commit SHA in the canonical checkpoint. -- [ ] [P2-T11] [expect-fail] Run the new router regression tests before the fix. Run +- [x] [P2-T11] [expect-fail] Run the new router regression tests before the fix. Run `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; $asm = Join-Path (Get-Location).Path "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll"; & $vstest $asm /InIsolation "/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterIssue637Tests&TestCategory!=LiveOutlook" /Logger:trx "/ResultsDirectory:coverage\testresults\p2-t11"; "EXIT_CODE=$LASTEXITCODE"'` and write `evidence/regression-testing/p2-t11-router-tests-red.md` with `ExpectedExitCode: 1`. Acceptance: the output does not contain `No test matches the given testcase filter`; the run @@ -973,7 +973,7 @@ commit SHA in the canonical checkpoint. `RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected`, `RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim`. A different partition is a defect in the tests, not evidence of the bug, and must be repaired before Phase 3. -- [ ] [P2-T12] [expect-fail] Run the new helper tests before the fix. Run the P2-T11 command with the +- [x] [P2-T12] [expect-fail] Run the new helper tests before the fix. Run the P2-T11 command with the filter substring changed to `FullyQualifiedName~EfcDataModelIssue637Tests` and the results directory changed to `coverage\testresults\p2-t12`, and write `evidence/regression-testing/p2-t12-helper-tests-red.md` with `ExpectedExitCode: 1`. Acceptance: @@ -981,7 +981,7 @@ commit SHA in the canonical checkpoint. total; exactly these 2 fail: `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` and `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem`; and the other 6 pass, because the seam already returns the input verbatim for every non-normalizable case. -- [ ] [P2-T13] Prove the new test file actually executes rather than silently compiling into nothing. +- [x] [P2-T13] Prove the new test file actually executes rather than silently compiling into nothing. From the TRX produced by P2-T11 at `coverage\testresults\p2-t11`, extract every `UnitTestResult` whose `testName` begins with one of the ten fixed method names, and write `evidence/regression-testing/p2-t13-compile-include-observed.md`. Quote only the `testName` and @@ -995,7 +995,7 @@ commit SHA in the canonical checkpoint. ### Phase 3 — Change A, producer normalization in `SelectRow` -- [ ] [P3-T1] Apply change A in `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`. Replace +- [x] [P3-T1] Apply change A in `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`. Replace the guard currently at lines 94-106 so that the `_boundRoot.Length != 0` and `ArchiveStemContract.IsFullOutlookPath(selection)` conjunction opens a block; inside that block, a failed `ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out string stem)` @@ -1018,7 +1018,7 @@ commit SHA in the canonical checkpoint. lines (the new one in `SelectRow` and the existing one at `:120` in `SelectHierarchyPath`); `rg -n "out _" QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` returns 0 lines; and `rg -c "IsFullOutlookPath" QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` returns 1. -- [ ] [P3-T2] Verify the nesting and the preserved diagnostics required by AC3 and AC6. Acceptance: +- [x] [P3-T2] Verify the nesting and the preserved diagnostics required by AC3 and AC6. Acceptance: `rg -n "Breadcrumb row rejected: target is outside the archive root." QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` returns exactly 1 line; `rg -n "Breadcrumb row rejected: target is the archive root itself." QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` @@ -1026,24 +1026,24 @@ commit SHA in the canonical checkpoint. `evidence/regression-testing/p3-t2-nesting.md` quotes the whole edited `SelectRow` body and records that `_boundRoot.Length != 0` is still the first conjunct, so the no-bound-root pass-through mode is untouched. -- [ ] [P3-T3] Verify AC8: `SelectHierarchyPath` and `CommitSelection` are unmodified. Run +- [x] [P3-T3] Verify AC8: `SelectHierarchyPath` and `CommitSelection` are unmodified. Run `git add QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` then `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` and write `evidence/regression-testing/p3-t3-selectionfile-diff.md`. Acceptance: every hunk header in the diff addresses a line range that lies entirely within the original lines 83 to 107; no hunk touches the original line range 109 to 139; and the artifact lists the hunk headers verbatim. -- [ ] [P3-T4] Run the analyzer build and the nullable build using the P0-T13 and P0-T14 commands +- [x] [P3-T4] Run the analyzer build and the nullable build using the P0-T13 and P0-T14 commands verbatim, and write `evidence/regression-testing/p3-t4-builds.md` recording both. Acceptance: both record `EXIT_CODE: 0`; both outputs contain `(Rebuild target(s))`; and neither recorded `Command:` line contains the solution-wide nullable opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. -- [ ] [P3-T5] Run the router regression suite green. Use the P2-T11 command with the results directory +- [x] [P3-T5] Run the router regression suite green. Use the P2-T11 command with the results directory changed to `coverage\testresults\p3-t5`, and write `evidence/regression-testing/p3-t5-router-tests-green.md`. Acceptance: `EXIT_CODE: 0`; 10 tests total; 10 passed; 0 failed; 0 skipped; and the five tests that failed in P2-T11 are named individually in the artifact as now passing. -- [ ] [P3-T6] Run the unmodified router test classes to prove no collateral regression. Use the P2-T11 +- [x] [P3-T6] Run the unmodified router test classes to prove no collateral regression. Use the P2-T11 command with the filter `"/TestCaseFilter:(FullyQualifiedName~BreadcrumbBridgeRouterIssue614Tests|FullyQualifiedName~BreadcrumbBridgeRouterTests|FullyQualifiedName~BreadcrumbBridgeRouterQueueTests)&TestCategory!=LiveOutlook"` and the results directory `coverage\testresults\p3-t6`, and write @@ -1055,7 +1055,7 @@ commit SHA in the canonical checkpoint. ### Phase 4 — Change B, normalization in the `string` overload of `MoveToFolderAsync` -- [ ] [P4-T1] Replace the seam body in `QuickFiler/Controllers/EfcDataModel.FilingStem.cs` with the +- [x] [P4-T1] Replace the seam body in `QuickFiler/Controllers/EfcDataModel.FilingStem.cs` with the real normalization. `ToFilingStemOrVerbatim` returns `candidatePath` unchanged when `ArchiveStemContract.IsFullOutlookPath(candidatePath)` is false; otherwise it calls `ArchiveStemContract.TryMakeArchiveRelative(candidatePath, archiveAncestor, out string stem)` and @@ -1093,7 +1093,7 @@ commit SHA in the canonical checkpoint. is at most 500; and `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler\Controllers\EfcDataModel.cs").Count'` reports exactly 485, because this task changes no line of that file. -- [ ] [P4-T2] Record the helper's line range and verify its purity, and write +- [x] [P4-T2] Record the helper's line range and verify its purity, and write `evidence/regression-testing/p4-t2-helper-shape.md`. Acceptance: the artifact records the first and last line numbers of the `ToFilingStemOrVerbatim` declaration body **in `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`**, and records separately that @@ -1106,17 +1106,17 @@ commit SHA in the canonical checkpoint. classifies each as the single declaration, the single call, or an XML-documentation reference, with exactly one declaration and exactly one call. A second call site anywhere in `QuickFiler/` fails this task. -- [ ] [P4-T3] Run the analyzer build and the nullable build using the P0-T13 and P0-T14 commands +- [x] [P4-T3] Run the analyzer build and the nullable build using the P0-T13 and P0-T14 commands verbatim, and write `evidence/regression-testing/p4-t3-builds.md`. Acceptance: both record `EXIT_CODE: 0`; both outputs contain `(Rebuild target(s))`; and neither recorded `Command:` line contains the solution-wide nullable opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. -- [ ] [P4-T4] Run the helper test class green. Use the P2-T12 command with the results directory +- [x] [P4-T4] Run the helper test class green. Use the P2-T12 command with the results directory changed to `coverage\testresults\p4-t4`, and write `evidence/regression-testing/p4-t4-helper-tests-green.md`. Acceptance: `EXIT_CODE: 0`; 8 tests total; 8 passed; 0 failed; and the two tests that failed in P2-T12 are named individually as now passing. -- [ ] [P4-T5] Prove the eight existing `ToArchiveRelativeStem` tests are unchanged and still pass. Use +- [x] [P4-T5] Prove the eight existing `ToArchiveRelativeStem` tests are unchanged and still pass. Use the P2-T11 command with the filter `"/TestCaseFilter:FullyQualifiedName~EfcDataModelIssue614Tests&TestCategory!=LiveOutlook"` and the results directory `coverage\testresults\p4-t5`, and write @@ -1126,7 +1126,7 @@ commit SHA in the canonical checkpoint. `git add QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` shows zero removed content lines. -- [ ] [P4-T6] Verify AC17: the non-goals are untouched. Run +- [x] [P4-T6] Verify AC17: the non-goals are untouched. Run `git add QuickFiler/Controllers/EfcDataModel.cs` then `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -U0 -- QuickFiler/Controllers/EfcDataModel.cs` and write `evidence/regression-testing/p4-t6-nongoals-untouched.md`, quoting every hunk header @@ -1157,7 +1157,7 @@ commit SHA in the canonical checkpoint. ### Phase 5 — Change C, the recorded spec correction to the issue #439 assertion -- [ ] [P5-T1] In `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, replace line 165 +- [x] [P5-T1] In `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, replace line 165 ` router.SelectedFolderPath.Should().Be(fullTarget);` with ` router.SelectedFolderPath.Should().Be(@"Clients\North");`. The expected value is derived from the fixture in the same method: `archiveRoot` is `@"\Archive"` at `:123` and @@ -1170,7 +1170,7 @@ commit SHA in the canonical checkpoint. returns exactly 1 line and it is line 165, and the fixed-string search `rg -F -n 'router.SelectedFolderPath.Should().Be(fullTarget);' QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` returns 0 lines. -- [ ] [P5-T2] Rename the enclosing method at line 119 from +- [x] [P5-T2] Rename the enclosing method at line 119 from `Issue439AlreadyRootedTargetRemainsUnchangedWithCaseInsensitiveArchiveMatch` to `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively`, on one line so the file line count is unchanged. Acceptance: @@ -1178,7 +1178,7 @@ commit SHA in the canonical checkpoint. returns 0 lines, and `rg -n "Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively" --glob "*.cs" .` returns exactly 1 line. -- [ ] [P5-T3] Narrow the arrange comment at lines 121-122 to the provider claim it still supports, +- [x] [P5-T3] Narrow the arrange comment at lines 121-122 to the provider claim it still supports, keeping it exactly two lines so the file line count is unchanged. The replacement text is: ` // Arrange: the presented target is rooted with casing different from the configured` and @@ -1194,13 +1194,13 @@ commit SHA in the canonical checkpoint. returns exactly 1 line and it is line 122, and `rg -n "already rooted with casing different" QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` returns 0 lines, recorded with `ExpectedExitCode: 1`. -- [ ] [P5-T4] Verify AC19: the companion provider assertion and `ToHierarchyPath` are preserved. Write +- [x] [P5-T4] Verify AC19: the companion provider assertion and `ToHierarchyPath` are preserved. Write `evidence/regression-testing/p5-t4-provider-assertion-preserved.md`. Acceptance: lines 161 to 164 of `BreadcrumbBridgeRouterIssue439Tests.cs` are byte-identical to their pre-change text, quoted in the artifact; and, after `git add QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`, `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -U0 -- QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` produces no output at all, since this plan changes no line of that file. -- [ ] [P5-T5] Verify the file did not grow and that exactly one assertion changed. Run +- [x] [P5-T5] Verify the file did not grow and that exactly one assertion changed. Run `pwsh -NoProfile -Command '(Get-Content -LiteralPath "QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs").Count'`, then `git add QuickFiler.Test` followed by `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- QuickFiler.Test`, and write @@ -1213,7 +1213,7 @@ commit SHA in the canonical checkpoint. half, and that this is explicitly not a weakened test; and the artifact additionally records the P5-T2 rename (both the removed and the added method name) and the two replacement comment lines from P5-T3, quoted verbatim, so that all three clauses of AC18 are evidenced in one artifact. -- [ ] [P5-T6] Run the issue #439 test class green. First re-run the P0-T13 analyzer build command +- [x] [P5-T6] Run the issue #439 test class green. First re-run the P0-T13 analyzer build command verbatim so that the Phase 5 test edits are compiled into `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`; record its `EXIT_CODE:` and its `(Rebuild target(s))` line in the same artifact. Without this rebuild the scoped run would @@ -1227,24 +1227,24 @@ commit SHA in the canonical checkpoint. ### Phase 6 — Change D, stale-comment cleanup -- [ ] [P6-T1] Replace `QuickFiler/Controllers/EfcSelectionGuard.cs:30` with the fixed replacement text +- [x] [P6-T1] Replace `QuickFiler/Controllers/EfcSelectionGuard.cs:30` with the fixed replacement text given in "Fixed identifiers", item 1. The surrounding claim that the guard still rejects rooted values stays as written; only the deferral wording changes. Acceptance: `rg -n "is implemented by issue #637" QuickFiler/Controllers/EfcSelectionGuard.cs` returns exactly 1 line, and `(Get-Content -LiteralPath "QuickFiler\Controllers\EfcSelectionGuard.cs").Count` is exactly 79. -- [ ] [P6-T2] Replace `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146` with the fixed +- [x] [P6-T2] Replace `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146` with the fixed replacement text given in "Fixed identifiers", item 2. Acceptance: `rg -n "the producer normalizes" QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` returns exactly 1 line, and it is line 146. -- [ ] [P6-T3] Replace the `because` string at `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:152` +- [x] [P6-T3] Replace the `because` string at `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:152` with the fixed replacement text given in "Fixed identifiers", item 3, on one line so the file line count is unchanged. Acceptance: `rg -n "the producer now normalizes before this predicate is reached" QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` returns exactly 1 line, and `(Get-Content -LiteralPath "QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs").Count` is exactly 296. -- [ ] [P6-T4] Verify the deferral is gone and the guard's behavior is unchanged. First re-run the +- [x] [P6-T4] Verify the deferral is gone and the guard's behavior is unchanged. First re-run the P0-T13 analyzer build command verbatim so that the P6-T2 and P6-T3 test edits are compiled into `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`; record its `EXIT_CODE:` and its `(Rebuild target(s))` line in the same artifact. Without this rebuild the scoped run would @@ -1263,7 +1263,7 @@ commit SHA in the canonical checkpoint. by a `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- QuickFiler/Controllers/EfcSelectionGuard.cs` run in the same task after `git add QuickFiler/Controllers/EfcSelectionGuard.cs`, whose only changed line is line 30. -- [ ] [P6-T5] Redact host identity from every evidence artifact written so far, then prove it. Apply +- [x] [P6-T5] Redact host identity from every evidence artifact written so far, then prove it. Apply the "Evidence transcript redaction" convention to every file already written under `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/`: replace every occurrence of the absolute worktree path with the literal `<worktree-root>`, and @@ -1285,7 +1285,7 @@ commit SHA in the canonical checkpoint. this task's own artifact is required for the same reason P7-T10 excludes its own: this artifact records its own two `Command:` lines, and those commands' patterns are the strings being searched for. No other evidence file of this feature is excluded. -- [ ] [P6-T6] Prepare the changes-A-through-D HEAD-materialization boundary. Write +- [x] [P6-T6] Prepare the changes-A-through-D HEAD-materialization boundary. Write `evidence/other/p6-t6-commit.md`, check off this task, and return `PROGRESS_COMMIT_REQUIRED: P2-T11..P6-T6` without invoking `git commit`. The orchestrator must stage `QuickFiler`, `QuickFiler.Test`, and this feature folder; collect canonical commit context; From 2a7eed0b8a0638ec8a0f83597e6b07fca12c920f Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 10:55:51 -0400 Subject: [PATCH 10/16] chore(quickfiler): record CSharpier formatter boundary - Apply and verify repository-wide CSharpier formatting - Capture P7 formatter evidence and mark completed plan tasks Refs: #637 --- .../BreadcrumbBridgeRouterIssue637Tests.cs | 5 +++- .../Controllers/EfcDataModelIssue614Tests.cs | 5 +++- .../BreadcrumbBridgeRouter.Selection.cs | 13 ++++---- .../qa-gates/p7-t1-csharpier-format.md | 30 +++++++++++++++++++ .../qa-gates/p7-t2-csharpier-check.md | 9 ++++++ .../plan.2026-08-29T12-20.md | 4 +-- 6 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t1-csharpier-format.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t2-csharpier-check.md diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs index 70e368b00..bff81acd1 100644 --- a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs @@ -195,7 +195,10 @@ private static string ToHierarchyPath(string presentedTarget, string archiveRoot return boundRoot + "\\" + presentedTarget; } - private static IReadOnlyList<FolderBreadcrumbSegment> Chain(string hierarchyPath, string archiveRoot) + private static IReadOnlyList<FolderBreadcrumbSegment> Chain( + string hierarchyPath, + string archiveRoot + ) { string boundRoot = archiveRoot.TrimEnd('\\', '/'); var chain = new List<FolderBreadcrumbSegment>(); diff --git a/QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs b/QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs index e273032b8..c6a0024dc 100644 --- a/QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs +++ b/QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs @@ -147,7 +147,10 @@ public void ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheSt [TestMethod] public void ToFilingStemOrVerbatim_RelativeStem_ReturnsTheInputVerbatim() { - EfcDataModel.ToFilingStemOrVerbatim(@"Clients\North", ArchiveRoot).Should().Be(@"Clients\North"); + EfcDataModel + .ToFilingStemOrVerbatim(@"Clients\North", ArchiveRoot) + .Should() + .Be(@"Clients\North"); } [TestMethod] diff --git a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs index 5b17a89fc..ca92725aa 100644 --- a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs +++ b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs @@ -92,12 +92,15 @@ private void SelectRow(BreadcrumbRow row) ? BreadcrumbRowBuilder.TrashRowText : row.FilingTarget; // #614 D2: normalize eligible rooted targets, preserving no-bound-root pass-through. - if ( - _boundRoot.Length != 0 - && ArchiveStemContract.IsFullOutlookPath(selection) - ) + if (_boundRoot.Length != 0 && ArchiveStemContract.IsFullOutlookPath(selection)) { - if (!ArchiveStemContract.TryMakeArchiveRelative(selection, _boundRoot, out string stem)) + if ( + !ArchiveStemContract.TryMakeArchiveRelative( + selection, + _boundRoot, + out string stem + ) + ) { log.Error("Breadcrumb row rejected: target is outside the archive root."); return; diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t1-csharpier-format.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t1-csharpier-format.md new file mode 100644 index 000000000..f31c2331b --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t1-csharpier-format.md @@ -0,0 +1,30 @@ +Timestamp: 2026-08-31T10-53 +Command: git status --porcelain -- QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 +EXIT_CODE: 0 +Before porcelain output: +(no output) + +Command: pwsh -NoProfile -Command 'dotnet tool run csharpier format .; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +Output: +Formatted 1564 files in 6281ms. +EXIT_CODE=0 + +Command: git status --porcelain -- QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 +EXIT_CODE: 0 +After porcelain output: + M QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs + M QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs + M QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs + M QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs + M QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs + M QuickFiler/Controllers/EfcDataModel.FilingStem.cs + M QuickFiler/Controllers/EfcDataModel.cs + M QuickFiler/Controllers/EfcSelectionGuard.cs + +Command: git status --porcelain -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization +EXIT_CODE: 0 +Out-of-scope porcelain output: +(no output) + +Output Summary: CSharpier formatted 1564 files and exited 0. The eight changed paths are all plan-owned source or test paths. BASELINE_FORMAT_DRIFT is empty, and the out-of-scope tree guard produced no output. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t2-csharpier-check.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t2-csharpier-check.md new file mode 100644 index 000000000..774090564 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t2-csharpier-check.md @@ -0,0 +1,9 @@ +Timestamp: 2026-08-31T10-54 +Command: pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +Output: +Checked 1564 files in 5483ms. +EXIT_CODE=0 + +Boundary-ready state: P7-T1 completed the write-mode formatting pass; this read-only check confirms no file would now be reformatted. The P7-T1..P7-T2 materialization boundary is ready for the orchestrator. +Output Summary: CSharpier check completed successfully across 1564 files without pending formatting changes. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index 5cdffb60b..8c73c62f1 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -1316,7 +1316,7 @@ commit SHA in the canonical checkpoint. Run the four steps in order. If any step fails or changes a file, return to P7-T1 and run the phase again from the start. -- [ ] [P7-T1] Format. Record +- [x] [P7-T1] Format. Record `git status --porcelain -- QuickFiler QuickFiler.Test docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` before the run, then run `pwsh -NoProfile -Command 'dotnet tool run csharpier format .; "EXIT_CODE=$LASTEXITCODE"'`, then @@ -1336,7 +1336,7 @@ again from the start. trees is invisible to them and would first surface at P8-T30, after the commit that could have carried it. Any line here means the repo-wide format pass touched a tree this plan does not own; stop and report to the orchestrator. -- [ ] [P7-T2] Verify the format. Run +- [x] [P7-T2] Verify the format. Run `pwsh -NoProfile -Command 'dotnet tool run csharpier check .; "EXIT_CODE=$LASTEXITCODE"'` and write `evidence/qa-gates/p7-t2-csharpier-check.md`. Acceptance: `EXIT_CODE: 0`, and the captured stdout is recorded verbatim. The exit code is the gate here rather than any summary wording, From 8c4e1d181dcf6673df442c49bc5b95092b043bd0 Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 11:04:18 -0400 Subject: [PATCH 11/16] chore(quickfiler): record post-change QA evidence - Capture analyzer, nullable, test, and coverage verification results - Mark P7-T3 through P7-T8 complete in the execution plan Refs: #637 --- .../qa-gates/p7-t3-msbuild-analyzers.md | 10 ++++++ .../qa-gates/p7-t4-msbuild-nullable.md | 10 ++++++ .../qa-gates/p7-t5-mstest-coverage.md | 21 +++++++++++++ .../qa-gates/p7-t6-coverage-headline.md | 4 +++ .../qa-gates/p7-t7-changed-line-coverage.md | 30 ++++++++++++++++++ .../evidence/qa-gates/p7-t8-coverage-delta.md | 31 +++++++++++++++++++ .../plan.2026-08-29T12-20.md | 12 +++---- 7 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t3-msbuild-analyzers.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t4-msbuild-nullable.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t5-mstest-coverage.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t6-coverage-headline.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t7-changed-line-coverage.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t8-coverage-delta.md diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t3-msbuild-analyzers.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t3-msbuild-analyzers.md new file mode 100644 index 000000000..3e3992d0c --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t3-msbuild-analyzers.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T10:57:10-04:00 +Command: pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +NULLABLE_OPT_IN_PROPERTY: absent +Output Summary: Build succeeded. MSBuild executed Rebuild target(s), reported 5 Warning(s), and reported 0 Error(s). + +MSBuild final status line: Build succeeded. +Warning(s): 5 +Error(s): 0 +(Rebuild target(s)): observed. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t4-msbuild-nullable.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t4-msbuild-nullable.md new file mode 100644 index 000000000..80fdd991f --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t4-msbuild-nullable.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T10:57:47-04:00 +Command: pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true; "EXIT_CODE=$LASTEXITCODE"' +EXIT_CODE: 0 +NULLABLE_OPT_IN_PROPERTY: absent +Output Summary: Build succeeded. MSBuild executed Rebuild target(s), reported 5 Warning(s), and reported 0 Error(s). + +MSBuild final status line: Build succeeded. +Warning(s): 5 +Error(s): 0 +(Rebuild target(s)): observed. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t5-mstest-coverage.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t5-mstest-coverage.md new file mode 100644 index 000000000..5e30d9916 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t5-mstest-coverage.md @@ -0,0 +1,21 @@ +Timestamp: 2026-08-31T10:59:40-04:00 +Command: pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-postchange.cobertura.xml +EXIT_CODE: 0 +COVERAGE_FLOOR_THROW: no +Output Summary: Discovered 9 test assemblies, matching the baseline. Test Run Successful: Total tests 6894; Passed 6894; Failed 0. The baseline failure set is empty, so no baseline failure remains. Coverage attributes: line-rate=0.853327; branch-rate=0.793089; lines-covered=54822; lines-valid=64245; branches-covered=13059; branches-valid=16466. Derived line coverage: 85.3327%. Derived branch coverage: 79.3089%. + +Baseline comparison: +- Discovered assemblies: baseline 9; post-change 9. +- Baseline failure set: empty. +- Post-change failures: 0. +- Baseline failures still failing: none. + +Coverage headline read-only substep: +- line-rate=0.853327 +- branch-rate=0.793089 +- lines-covered=54822 +- lines-valid=64245 +- branches-covered=13059 +- branches-valid=16466 +- Derived line coverage: 85.3327% +- Derived branch coverage: 79.3089% diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t6-coverage-headline.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t6-coverage-headline.md new file mode 100644 index 000000000..d55e3f454 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t6-coverage-headline.md @@ -0,0 +1,4 @@ +Timestamp: 2026-08-31T11:00:26-04:00 +Command: pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p7-t5-postchange.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; $c = $d.SelectSingleNode("/coverage"); foreach ($a in @("line-rate","branch-rate","lines-covered","lines-valid","branches-covered","branches-valid")) { $a + "=" + $c.GetAttribute($a) }' +EXIT_CODE: 0 +Output Summary: line-rate=0.853327; branch-rate=0.793089; lines-covered=54822; lines-valid=64245; branches-covered=13059; branches-valid=16466. Derived line coverage: 85.3327%. Derived branch coverage: 79.3089%. All six attributes and both derived percentages equal the P7-T5 artifact. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t7-changed-line-coverage.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t7-changed-line-coverage.md new file mode 100644 index 000000000..9e490cc71 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t7-changed-line-coverage.md @@ -0,0 +1,30 @@ +Timestamp: 2026-08-31T11:01:18-04:00 +Command 1: pwsh -NoProfile -Command '. ".\scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1"; $raw = Get-Content -LiteralPath ".\coverage\p7-t5-postchange.cobertura.xml" -Raw -Encoding UTF8; [xml]$d = ConvertTo-KoverageCoberturaXml -XmlContent $raw -RepoRoot (Get-Location).Path; foreach ($f in @("QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\EfcDataModel.FilingStem.cs")) { $u = @(); foreach ($c in $d.SelectNodes("//class")) { if ($c.GetAttribute("filename") -eq $f) { foreach ($l in $c.SelectNodes("./lines/line")) { if ([int]$l.GetAttribute("hits") -eq 0) { $u += [int]$l.GetAttribute("number") } } } }; $f + " uncovered=" + (($u | Sort-Object -Unique) -join ",") }' +Command 2: git diff -U0 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/EfcDataModel.cs QuickFiler/Controllers/EfcDataModel.FilingStem.cs +EXIT_CODE: 0 +Output Summary: No changed line is uncovered. The new helper has coverage rows on 13 lines, all with non-zero hits. Its `IsFullOutlookPath` conditional is recorded as branch=True with condition-coverage 100% (6/6), demonstrating both conditional outcomes. + +Added lines from the anchored diff: +- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs: 97-115. +- QuickFiler/Controllers/EfcDataModel.cs: 21, 337. +- QuickFiler/Controllers/EfcDataModel.FilingStem.cs: 1-29. + +Coverage rows with zero hits: +- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs: 29, 30, 205. +- QuickFiler/Controllers/EfcDataModel.cs: 181, 183-185, 189-195, 197-208, 210-221, 248-250, 253-254, 345-346, 362-368, 370-371, 386-392, 394-395, 406-419, 451-454, 457-458, 460, 463-466, 467, 469-475, 478, 480-481. +- QuickFiler/Controllers/EfcDataModel.FilingStem.cs: none. + +Changed-line intersections: +- QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs: empty. +- QuickFiler/Controllers/EfcDataModel.cs: empty. +- QuickFiler/Controllers/EfcDataModel.FilingStem.cs: empty. + +ToFilingStemOrVerbatim range: +- Re-derived declaration line: 11. +- Re-derived closing-brace line: 27. +- P4-T2 recorded range: 11-27. +- Comparison: unchanged after formatting. +- Coverage rows within the re-derived range: 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 26; each has hits=1. +- Lines with no coverage row: 11 (method signature), 24 (brace), 25 (blank line), 27 (brace). +- At least one range line carries a coverage row: yes. +- Branch coverage row: line 13 has branch="True" and condition-coverage="100% (6/6)". This is the `IsFullOutlookPath` conditional and records both outcomes. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t8-coverage-delta.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t8-coverage-delta.md new file mode 100644 index 000000000..7dd6d316f --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t8-coverage-delta.md @@ -0,0 +1,31 @@ +Timestamp: 2026-08-31T11:02:14-04:00 + +## Baseline coverage + +Source: `evidence/baseline/p0-t16-coverage-headline.md`. + +- line-rate=0.853428; line coverage=85.3428% +- branch-rate=0.793049; branch coverage=79.3049% +- lines-covered=54808; lines-valid=64221 +- branches-covered=13052; branches-valid=16458 + +## Post-change coverage + +Source: `evidence/qa-gates/p7-t6-coverage-headline.md`. + +- line-rate=0.853327; line coverage=85.3327% +- branch-rate=0.793089; branch coverage=79.3089% +- lines-covered=54822; lines-valid=64245 +- branches-covered=13059; branches-valid=16466 + +## Changed and new-code coverage + +Source: `evidence/qa-gates/p7-t7-changed-line-coverage.md`. + +- Added-line and zero-hit intersections are empty for all three changed production files. +- Every emitted Cobertura sequence point within `ToFilingStemOrVerbatim` has non-zero hits. +- The helper conditional has `branch="True"` and `condition-coverage="100% (6/6)"`, demonstrating both outcomes. + +Coverage-floor authority: the General Unit Test Policy in `AGENTS.md` and `.agents/skills/csharp/SKILL.md` require repository-wide line coverage at or above 80 percent, new modules, classes and methods to target at least 90 percent coverage, and no coverage regression on changed lines. `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1:487` enforces the same repository-wide 80-percent figure. + +Result: post-change line coverage is 85.3327%, which is at or above 80%. The changed-line uncovered intersection is empty, and the new helper's emitted sequence points are all covered. `BASELINE BELOW FLOOR` does not apply. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index 8c73c62f1..53dea5a41 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -1350,19 +1350,19 @@ again from the start. every subsequent `0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD` diff describes the same file contents the P7-T5 build measures. If CSharpier changed no source, the evidence and plan check-off still make the boundary non-empty; the artifact records that the source already matched `HEAD`. -- [ ] [P7-T3] Analyzers. Run the P0-T13 command verbatim and write +- [x] [P7-T3] Analyzers. Run the P0-T13 command verbatim and write `evidence/qa-gates/p7-t3-msbuild-analyzers.md`. Acceptance: `EXIT_CODE: 0`; the output contains `(Rebuild target(s))`; the `Error(s)` count is 0; and the recorded `Command:` line contains `/t:Rebuild` and `EnableNETAnalyzers=true` and `EnforceCodeStyleInBuild=true` and contains neither `/t:Build` nor the solution-wide nullable opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. -- [ ] [P7-T4] Nullable. Run the P0-T14 command verbatim and write +- [x] [P7-T4] Nullable. Run the P0-T14 command verbatim and write `evidence/qa-gates/p7-t4-msbuild-nullable.md`. Acceptance: `EXIT_CODE: 0`; the output contains `(Rebuild target(s))`; and the recorded `Command:` line contains `/t:Rebuild` and `TreatWarningsAsErrors=true` and contains neither `/t:Build` nor the solution-wide nullable opt-in property — record this as `NULLABLE_OPT_IN_PROPERTY: absent`; do not spell the token in the artifact. -- [ ] [P7-T5] Full test run with coverage. Run +- [x] [P7-T5] Full test run with coverage. Run `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-postchange.cobertura.xml` and write `evidence/qa-gates/p7-t5-mstest-coverage.md`. The exit-code condition is stated against the recorded baseline rather than as an unconditional zero, because the wrapper throws @@ -1387,14 +1387,14 @@ again from the start. read-only P7-T5 substep, run the P7-T6 coverage-headline command against `coverage\p7-t5-postchange.cobertura.xml`, record those values in this P7-T5 `Output Summary:`, verify P7-T5, and check P7-T5 off before starting P7-T6. P7-T6 does not modify P7-T5. -- [ ] [P7-T6] Read the post-change numeric coverage headline. Run the P0-T16 command with the input +- [x] [P7-T6] Read the post-change numeric coverage headline. Run the P0-T16 command with the input path changed to `.\coverage\p7-t5-postchange.cobertura.xml` and write `evidence/qa-gates/p7-t6-coverage-headline.md`. Acceptance: `EXIT_CODE: 0`, and `Output Summary:` records all six numeric attribute values plus the derived line-coverage percentage and branch percentage. Confirm that all six attributes and both derived percentages equal the values already recorded in `evidence/qa-gates/p7-t5-mstest-coverage.md`; P7-T6 does not modify that artifact or any prior checklist state. -- [ ] [P7-T7] Verify changed-line coverage. Run the P0-T17 command with the input path changed to +- [x] [P7-T7] Verify changed-line coverage. Run the P0-T17 command with the input path changed to `.\coverage\p7-t5-postchange.cobertura.xml` and with its file list extended to the three production files this plan touches — `QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs`, `QuickFiler\Controllers\EfcDataModel.cs` and @@ -1449,7 +1449,7 @@ again from the start. `branch="True"` node exists in the re-derived range, and the witness-test form applies only when none does. The `HEAD` this diff is anchored against already carries the formatting result, because P7-T2 committed it before P7-T5 measured the tree. -- [ ] [P7-T8] Report the coverage delta. Write `evidence/qa-gates/p7-t8-coverage-delta.md` containing +- [x] [P7-T8] Report the coverage delta. Write `evidence/qa-gates/p7-t8-coverage-delta.md` containing three labelled numeric sections: baseline coverage, copied from `evidence/baseline/p0-t16-coverage-headline.md`; post-change coverage, copied from `evidence/qa-gates/p7-t6-coverage-headline.md`; and changed and new-code coverage, copied from From 810efce459c5d9377248333aa43e7256d7dccc8a Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 11:08:09 -0400 Subject: [PATCH 12/16] docs(quickfiler): record Phase 7 QA audit evidence - Capture file-size, toolchain, and evidence-redaction audits - Mark P7-T9 through P7-T12 complete for the commit boundary Refs: #637 --- .../evidence/other/p7-t12-commit.md | 23 +++++++++++ .../qa-gates/p7-t10-toolchain-audit.md | 40 +++++++++++++++++++ .../qa-gates/p7-t11-evidence-redaction.md | 13 ++++++ .../evidence/qa-gates/p7-t9-file-sizes.md | 27 +++++++++++++ .../plan.2026-08-29T12-20.md | 8 ++-- 5 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p7-t12-commit.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t10-toolchain-audit.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t11-evidence-redaction.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t9-file-sizes.md diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p7-t12-commit.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p7-t12-commit.md new file mode 100644 index 000000000..d8f73d28e --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p7-t12-commit.md @@ -0,0 +1,23 @@ +Timestamp: 2026-08-31T11-05 +Command: git status --porcelain -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 + +Production porcelain output: + +``` +(no output) +``` + +Command: git status --porcelain -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 +EXIT_CODE: 0 + +Feature-folder porcelain output before the boundary commit: + +``` + M docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +?? docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t10-toolchain-audit.md +?? docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t11-evidence-redaction.md +?? docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t9-file-sizes.md +``` + +The production scopes are clean. The feature-folder changes are the plan check-offs and the P7-T9 through P7-T11 evidence that must be materialized by this boundary. After the orchestrator's boundary commit, it must record the interval and non-empty SHA in `artifacts/orchestration/orchestrator-state.json`, verify the production porcelain output remains empty, and verify that this artifact and the plan are the only allowable remaining feature-folder paths. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t10-toolchain-audit.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t10-toolchain-audit.md new file mode 100644 index 000000000..ebf8e9ab9 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t10-toolchain-audit.md @@ -0,0 +1,40 @@ +Timestamp: 2026-08-31T11-05 +Command: pwsh -NoProfile -Command 'if (Get-Command msbuild -ErrorAction SilentlyContinue) { "ON_PATH" } else { "NOT_ON_PATH" }' +EXIT_CODE: 0 + +## Final-QC command audit + +The final-QC commands were recorded in this order: + +1. `dotnet tool run csharpier format .` +2. `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; "EXIT_CODE=$LASTEXITCODE"'` +3. `pwsh -NoProfile -Command '$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\\Installer\\vswhere.exe"; $msbuildExe = & $vswhere -latest -requires Microsoft.Component.MSBuild -find "MSBuild\\**\\Bin\\MSBuild.exe" | Select-Object -First 1; & $msbuildExe TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true; "EXIT_CODE=$LASTEXITCODE"'` +4. `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-postchange.cobertura.xml` + +The format step uses the manifest-pinned tool through `dotnet tool run`; no global CSharpier binary was used. Both MSBuild commands carry `/t:Rebuild`, not `/t:Build`; their source transcripts record `(Rebuild target(s)): observed.` + +## Nullable opt-in token discipline + +Command: `rg -n 'Nullable=enable' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**/*.md' --glob '!**/p7-t10-toolchain-audit.md'` + +EXIT_CODE: 1 + +ExpectedExitCode: 1 + +Output Summary: The scan returned zero matches across the feature evidence subtree other than this artifact. All final-QC evidence records `NULLABLE_OPT_IN_PROPERTY: absent` instead of the solution-wide opt-in token. + +## MSBuild resolution + +The PATH probe returned `ON_PATH`. + +Command: `pwsh -NoProfile -Command '(Get-Command msbuild).Source'` + +EXIT_CODE: 0 + +Output: `C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe` + +The vswhere resolution produced the same binary. The vswhere-resolved absolute path remains in use for determinism because the repository does not pin the PATH entry. + +## Test-runner substitution + +The test command is `scripts/vscode/Invoke-MSTestWithCoverage.ps1`. Its inner vstest invocation at `Invoke-MSTestWithCoverage.ps1:76` carries `/InIsolation` and `/TestCaseFilter:TestCategory!=LiveOutlook`; coverage is collected with `dotnet-coverage --output-format cobertura`, not `/EnableCodeCoverage`. This is the repository-standard runner and corresponds to `.github/workflows/_mstest-coverage.yml:83`. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t11-evidence-redaction.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t11-evidence-redaction.md new file mode 100644 index 000000000..74ce605d0 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t11-evidence-redaction.md @@ -0,0 +1,13 @@ +Timestamp: 2026-08-31T11-05 +Command (search 1): `rg -F -n 'C:\Users' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**' --glob '!**/p7-t11-evidence-redaction.md' --glob '!**/p6-t5-evidence-redaction.md'` +ExpectedExitCode (search 1): 1 +Command (search 2): `rg -F -n '.trx' docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637 --glob '**/evidence/**' --glob '!**/p7-t11-evidence-redaction.md' --glob '!**/p6-t5-evidence-redaction.md'` +ExpectedExitCode (search 2): 1 + +Output Summary: Both whole-feature evidence scans returned zero matches with exit code 1. + +## Evidence transcript redaction + +No evidence file required a rewrite during P7-T11. The feature evidence tree, excluding only this self-referential scan artifact and the equivalent P6-T5 artifact, already contains no absolute per-user profile path and no vstest results-file suffix. + +The scans cover the full feature evidence tree, re-verifying the Phase 0 through Phase 6 evidence that P6-T5 cleared as well as every Phase 7 artifact written before this task. The two exclusions are required because each records the literal search patterns used to prove its own result. No other evidence artifact is excluded. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t9-file-sizes.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t9-file-sizes.md new file mode 100644 index 000000000..30145376a --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p7-t9-file-sizes.md @@ -0,0 +1,27 @@ +Timestamp: 2026-08-31T11-05 +Command: pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\\Controllers\\EfcDataModel.cs","QuickFiler\\Controllers\\EfcDataModel.FilingStem.cs","QuickFiler\\Controllers\\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\\Controllers\\EfcSelectionGuard.cs","QuickFiler.Test\\Controllers\\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\\Controllers\\BreadcrumbBridgeRouterIssue637Tests.cs","QuickFiler.Test\\Controllers\\EfcDataModelIssue614Tests.cs","QuickFiler.Test\\Controllers\\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }' +EXIT_CODE: 0 + +## Output + +``` +QuickFiler\Controllers\EfcDataModel.cs=485 +QuickFiler\Controllers\EfcDataModel.FilingStem.cs=29 +QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs=221 +QuickFiler\Controllers\EfcSelectionGuard.cs=79 +QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs=694 +QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs=254 +QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs=196 +QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs=296 +``` + +## Verification + +- `EfcDataModel.cs` is 485 lines, at or below 500 and unchanged from the plan's expected 485. +- `EfcDataModel.FilingStem.cs` is 29 lines, at or below 500. +- `BreadcrumbBridgeRouter.Selection.cs` is 221 lines, at or below 500. +- `EfcSelectionGuard.cs` is 79 lines, at or below 79. +- `BreadcrumbBridgeRouterIssue439Tests.cs` is 694 lines, at or below 694. +- `BreadcrumbBridgeRouterIssue637Tests.cs` is 254 lines, at or below 500. +- `EfcDataModelIssue614Tests.cs` is 196 lines, at or below 500. +- `EfcSelectionGuardTests.cs` is 296 lines, at or below 296. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index 53dea5a41..9e1d3fcd2 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -1478,7 +1478,7 @@ again from the start. canonical commit context and the routed commit-steward profile, then recorded the boundary SHA and all intermediate HEAD-materialization SHAs within this task interval in the canonical checkpoint. -- [ ] [P7-T9] File-size audit, run after the formatter rather than before it, because CSharpier can +- [x] [P7-T9] File-size audit, run after the formatter rather than before it, because CSharpier can change a file's line count. Run `pwsh -NoProfile -Command 'foreach ($p in @("QuickFiler\Controllers\EfcDataModel.cs","QuickFiler\Controllers\EfcDataModel.FilingStem.cs","QuickFiler\Controllers\BreadcrumbBridgeRouter.Selection.cs","QuickFiler\Controllers\EfcSelectionGuard.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue439Tests.cs","QuickFiler.Test\Controllers\BreadcrumbBridgeRouterIssue637Tests.cs","QuickFiler.Test\Controllers\EfcDataModelIssue614Tests.cs","QuickFiler.Test\Controllers\EfcSelectionGuardTests.cs")) { $p + "=" + (Get-Content -LiteralPath $p).Count }'` and write `evidence/qa-gates/p7-t9-file-sizes.md`. Acceptance: `EfcDataModel.cs` is at most 500, @@ -1492,7 +1492,7 @@ intermediate HEAD-materialization SHAs within this task interval in the canonica `BreadcrumbBridgeRouter.Selection.cs` is at most 500. The upper bounds replace exact equalities because this task runs after a write-mode formatter that can reduce a line count, and AC25 requires only that these files not grow. -- [ ] [P7-T10] Toolchain non-vacuity audit. Write `evidence/qa-gates/p7-t10-toolchain-audit.md` +- [x] [P7-T10] Toolchain non-vacuity audit. Write `evidence/qa-gates/p7-t10-toolchain-audit.md` enumerating the four final-QC command steps in order with their recorded `Command:` lines quoted verbatim. Acceptance: the format step is `dotnet tool run csharpier format .` invoked through `dotnet tool run` and not through a globally installed binary; both MSBuild `Command:` lines @@ -1534,7 +1534,7 @@ intermediate HEAD-materialization SHAs within this task interval in the canonica `dotnet-coverage --output-format cobertura` rather than by `/EnableCodeCoverage`, which is the repository's standard runner and the local analogue of `.github/workflows/_mstest-coverage.yml:83`. The substitutions are recorded, not resolved. -- [ ] [P7-T11] Redact host identity from the Phase 7 evidence artifacts, then prove it. Apply the +- [x] [P7-T11] Redact host identity from the Phase 7 evidence artifacts, then prove it. Apply the "Evidence transcript redaction" convention to every file written under `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/` since P6-T5 ran: replace every occurrence of the absolute worktree path with the literal @@ -1558,7 +1558,7 @@ intermediate HEAD-materialization SHAs within this task interval in the canonica artifact and `evidence/other/p6-t5-evidence-redaction.md`, which P6-T5 wrote earlier in the same feature tree. Omitting the second exclusion would make this gate unsatisfiable. No other evidence file of this feature is excluded. -- [ ] [P7-T12] Prepare the QA-evidence HEAD-materialization boundary. Write +- [x] [P7-T12] Prepare the QA-evidence HEAD-materialization boundary. Write `evidence/other/p7-t12-commit.md`, check off this task, and return `PROGRESS_COMMIT_REQUIRED: P7-T9..P7-T12` without invoking `git commit`. The orchestrator must stage `QuickFiler`, `QuickFiler.Test`, and this feature folder; collect canonical commit context; From 1deca49e7486df09e9b24d222c0d02d1ecd1d08a Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 12:21:11 -0400 Subject: [PATCH 13/16] docs(issue-637): reconcile acceptance evidence after main merge - Record the AC21 invariant correction and derived AC14 test evidence - Reconcile plan anchors, scope validation, routing receipts, and 30 AC checkoffs Refs: #637 --- .../other/p8-t21-spec-correction-record.md | 5 + .../p8-t16-movetofolder-family-post.md | 23 +++ .../p8-t24-selectedfolderpath-post.md | 13 ++ .../qa-gates/p8-t30-scope-boundary.md | 39 +++++ .../qa-gates/p8-t31-ac-reconciliation.md | 10 ++ .../qa-gates/p8-t9-selection-family-post.md | 20 +++ ...p4-t4-helper-tests-ac14-derived-summary.md | 12 ++ .../p4-t4-helper-tests-derived-summary.md | 10 ++ .../plan.2026-08-29T12-20.md | 152 ++++++++++-------- .../spec.md | 60 +++---- 10 files changed, 251 insertions(+), 93 deletions(-) create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t21-spec-correction-record.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t16-movetofolder-family-post.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t24-selectedfolderpath-post.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t30-scope-boundary.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t31-ac-reconciliation.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t9-selection-family-post.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-ac14-derived-summary.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-derived-summary.md diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t21-spec-correction-record.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t21-spec-correction-record.md new file mode 100644 index 000000000..0e4a02422 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t21-spec-correction-record.md @@ -0,0 +1,5 @@ +Timestamp: 2026-08-31T14:04:00-04:00 +Source: `evidence/regression-testing/p5-t5-single-assertion-change.md` +Output Summary: This is the designated source text for the pull-request change description. + +The issue #439 criterion that a rooted target survives selection is superseded by issue #614's archive-relative-stem invariant, which #614 enforced on the `SelectHierarchyPath` half and at the filing boundary but not on the `SelectRow` half. This is a deliberate spec correction and explicitly not a weakened test. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t16-movetofolder-family-post.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t16-movetofolder-family-post.md new file mode 100644 index 000000000..1287385c9 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t16-movetofolder-family-post.md @@ -0,0 +1,23 @@ +Timestamp: 2026-08-31T14:03:00-04:00 +Command 1: `rg -n "MoveToFolder" --glob "*.cs" .` +Command 2: `rg -n "MoveToFolderAsync\s*\(" --glob "*.cs" .` +EXIT_CODE: 0 for both commands. +Output Summary: The family-stem search returned 23 lines across 6 files. The syntax-anchored search returned 10 lines across 5 files: 3 declarations and 7 call sites. No new overload or signature change appears. + +Syntax-anchored declarations: + +- `QuickFiler/Controllers/EfcDataModel.cs:303` +- `QuickFiler/Controllers/EfcDataModel.cs:398` +- `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:89` + +Syntax-anchored call sites: + +- `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:78` +- `QuickFiler/Controllers/EfcHomeController.ExecuteMoves.cs:98` +- `QuickFiler/Controllers/EfcDataModel.cs:408` +- `QuickFiler/Controllers/EfcFormController.cs:537` +- `QuickFiler/Controllers/EfcFormController.cs:844` +- `QuickFiler.Test/Controllers/EfcHomeControllerExecuteMovesTests.cs:87` +- `QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs:314` + +AC16 already carries these measured figures. P8-T32 list entry A9 re-verifies them. The 16-line figure retained at `spec.md:313` describes the pre-#638 tree and is recorded in P8-T32 list entry B6. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t24-selectedfolderpath-post.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t24-selectedfolderpath-post.md new file mode 100644 index 000000000..8a98048d6 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t24-selectedfolderpath-post.md @@ -0,0 +1,13 @@ +Timestamp: 2026-08-31T14:05:00-04:00 +Command 1: `rg -c "SelectedFolderPath" --glob "*.cs" .` +Command 2: `rg -n "SelectedFolderPath" --glob "*.cs" QuickFiler/ UtilitiesCS/ TaskMaster/ ToDoModel/ Tags/ TaskVisualization/` +Command 3: `rg -n "public string\? SelectedFolderPath \{ get; private set; \}" QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` +EXIT_CODE: 0 for all commands. +Output Summary: The post-change production surface is 9 lines across 3 files. It has 2 writes and 3 reads. The property declaration pattern has exactly one match and retains `private set`. + +- Declaration: `BreadcrumbBridgeRouter.cs:59`. +- Documentation and event-only references: `BreadcrumbBridgeRouter.cs:61`, `:62`, `:146`; `BreadcrumbBridgeRouter.Selection.cs:150`. +- Writes: `BreadcrumbBridgeRouter.cs:145`; `BreadcrumbBridgeRouter.Selection.cs:146`. +- Reads: `BreadcrumbBridgeRouter.cs:143`; `BreadcrumbBridgeRouter.Selection.cs:150`; `EfcFormController.cs:321`. + +No new write site or public API member appears. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t30-scope-boundary.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t30-scope-boundary.md new file mode 100644 index 000000000..60ca966a8 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t30-scope-boundary.md @@ -0,0 +1,39 @@ +Timestamp: 2026-08-31T14:06:00-04:00 +Main parent anchor: `3be3f237a8551df3f27f83d9d1af2f26074fc93a` + +Command 1: `git status --porcelain -- QuickFiler QuickFiler.Test UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization` +EXIT_CODE: 0 +Output: + +``` + +``` + +Command 2: `git diff --name-only 3be3f237a8551df3f27f83d9d1af2f26074fc93a..HEAD -- QuickFiler QuickFiler.Test` +EXIT_CODE: 0 +Output: + +``` +QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs +QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs +QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs +QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs +QuickFiler.Test/QuickFiler.Test.csproj +QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs +QuickFiler/Controllers/EfcDataModel.FilingStem.cs +QuickFiler/Controllers/EfcDataModel.cs +QuickFiler/Controllers/EfcSelectionGuard.cs +QuickFiler/QuickFiler.csproj +``` + +Command 3: `git diff --name-only 3be3f237a8551df3f27f83d9d1af2f26074fc93a..HEAD -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization` +EXIT_CODE: 0 +Output: + +``` + +``` + +Output Summary: The porcelain span is empty. The anchored issue #637 diff contains exactly the ten P6-T6 paths. `BASELINE_FORMAT_DRIFT` is empty, so no formatter-only addition applies. The other product-tree diff is empty. Phase 8 evidence and AC checkbox edits are intentionally under the omitted feature-folder path. + +Scope division: P6-T6 committed changes A through D; P7-T2 committed the formatting result; P7-T12 committed Phase 7 evidence; the named main parent excludes subsequent main-owned changes. Therefore the anchored diffs enumerate issue #637 changes only. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t31-ac-reconciliation.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t31-ac-reconciliation.md new file mode 100644 index 000000000..21d13cbb9 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t31-ac-reconciliation.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T14:07:00-04:00 +Construction 1: Extracted the lines between `^## Acceptance Criteria$` and `^## Risks & Mitigations$`, then counted `^- \[x\] AC` and `^- \[ \] AC` within that range. +Result: 30 checked; 0 unchecked. + +Construction 2: Counted `^- \[x\] AC` and `^- \[ \] AC` over the full file. +Result: 30 checked; 0 unchecked. + +Output Summary: Both independently constructed, section-scoped counts agree: all 30 acceptance criteria are checked off and none remains unchecked. + +An unscoped count of every checkbox line reports 35 and over-reports the AC total by 5. The five non-AC checkboxes are at `spec.md:54`, `:55`, `:56`, `:57`, and `:86`. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t9-selection-family-post.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t9-selection-family-post.md new file mode 100644 index 000000000..2a7fef19f --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p8-t9-selection-family-post.md @@ -0,0 +1,20 @@ +Timestamp: 2026-08-31T14:10:00-04:00 +Search 1: `rg -n "Select(Row|HierarchyPath)\\s*\\(" --glob "*.cs" QuickFiler/Controllers/BreadcrumbBridgeRouter.cs QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` +Search 2: `rg -n "^\\s+(private void )?Select(Row|HierarchyPath)\\(" --glob "*.cs" .` +EXIT_CODE: 0 for both searches +Output Summary: Both searches returned the identical nine-line set: two declarations and seven call sites. No Family-B `SelectRow(int)` member appears. + +Declarations: + +- `BreadcrumbBridgeRouter.Selection.cs:83 SelectRow` +- `BreadcrumbBridgeRouter.Selection.cs:121 SelectHierarchyPath` + +Call sites: + +- `BreadcrumbBridgeRouter.Selection.cs:33` +- `BreadcrumbBridgeRouter.Selection.cs:47` +- `BreadcrumbBridgeRouter.cs:201` +- `BreadcrumbBridgeRouter.cs:286` +- `BreadcrumbBridgeRouter.Arrows.cs:138` +- `BreadcrumbBridgeRouter.Arrows.cs:153` +- `BreadcrumbBridgeRouter.Arrows.cs:161` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-ac14-derived-summary.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-ac14-derived-summary.md new file mode 100644 index 000000000..e5a538c82 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-ac14-derived-summary.md @@ -0,0 +1,12 @@ +Timestamp: 2026-08-31T14:02:00-04:00 +Source: Retained P4-T4 TRX result XML, identified only by SHA256 `D55969654086D9C56BD799F313CEE2A0FB9A6C17FDE0C2631050DEFE8077374C`. +Source hash verification: Verified before parsing. +Method: Paired each retained `UnitTestResult` `testName` and `outcome` attribute with its `UnitTest/TestMethod` `className`. +Output Summary: All four AC14 retained results passed. + +- `QuickFiler.Test.Controllers.EfcDataModelIssue637Tests.ToFilingStemOrVerbatim_ArchiveRootExact_ReturnsTheInputVerbatimAndDoesNotThrow` — `Passed` +- `QuickFiler.Test.Controllers.EfcDataModelIssue637Tests.ToFilingStemOrVerbatim_OutOfRootRootedInput_ReturnsTheInputVerbatimAndDoesNotThrow` — `Passed` +- `QuickFiler.Test.Controllers.EfcDataModelIssue637Tests.ToFilingStemOrVerbatim_NullEmptyWhitespaceOrSeparatorOnlyAncestor_ReturnsTheInputVerbatim` — `Passed` +- `QuickFiler.Test.Controllers.EfcDataModelIssue637Tests.ToFilingStemOrVerbatim_NullOrEmptyCandidate_ReturnsTheInputVerbatim` — `Passed` + +This supplemental derived summary does not modify the retained TRX or either existing P4-T4 Markdown summary. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-derived-summary.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-derived-summary.md new file mode 100644 index 000000000..8098de7ae --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/regression-testing/p4-t4-helper-tests-derived-summary.md @@ -0,0 +1,10 @@ +Timestamp: 2026-08-31T11-14 +Source: Retained P4-T4 TRX result XML at `coverage/testresults/p4-t4/<trx-file>`. +Source SHA256: `D55969654086D9C56BD799F313CEE2A0FB9A6C17FDE0C2631050DEFE8077374C` +Method: Parsed the retained result XML's `UnitTestResult` records by exact `testName` and recorded the corresponding `outcome` values. +Output Summary: The retained P4-T4 result contains 8 total, 8 executed, 8 passed, and 0 failed tests. + +- `ToFilingStemOrVerbatim_RelativeStem_ReturnsTheInputVerbatim` — `Passed` (one matching result). +- `ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim` — `Passed` (one matching result). + +This supplemental summary corrects the two test-name omissions in `p4-t4-helper-tests-green.md`. It does not modify the preserved P4-T4 stdout summary or the retained source result XML. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index 9e1d3fcd2..a162ca85f 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -3,9 +3,9 @@ - **Issue:** #637 - **Parent (optional):** none - **Owner:** drmoisan -- **Last Updated:** 2026-08-31T10-22 +- **Last Updated:** 2026-08-31T11-14 - **Status:** Ready for Codex preflight -- **Version:** 0.8 +- **Version:** 0.9 - **Work Mode:** full-bug (from `issue.md`); `spec.md` is the sole acceptance-criteria source (AC1-AC30). ## Conventions (read before executing any task) @@ -28,11 +28,13 @@ would embed a host account name. The placeholder convention this feature already path in prose is the one at `research/research.2026-08-29T12-30.md:6`, `<repo-root>/.claude/worktrees/<worktree-id>`. -**Base commit** — the diff anchor for this plan is the literal commit -`0eda184ca0009bc79ac9b7146897270c17c095fa`. Every `git diff` in this plan supplies it explicitly. No -task pins a HEAD SHA: no acceptance condition in this plan asserts that `HEAD` equals a stated -value, and P0-T8 records `git rev-parse HEAD` as an observation rather than as a gate. This anchor -is the third this plan has carried, and both supersessions have the same cause. The plan was first +**Base commit** — completed P0 through P7 tasks retain their historical literal diff anchor +`0eda184ca0009bc79ac9b7146897270c17c095fa`; their recorded evidence must not be reinterpreted or +replayed. The remaining P8-T30 scope gate uses the literal post-merge `main` parent +`3be3f237a8551df3f27f83d9d1af2f26074fc93a`. No task pins a HEAD SHA: no acceptance condition in +this plan asserts that `HEAD` equals a stated value, and P0-T8 records `git rev-parse HEAD` as an +observation rather than as a gate. The historical anchor is the third this plan has carried, and +both supersessions had the same cause. The plan was first authored against `ecdb1c84`. `origin/main` then advanced to `fa2ddefa` (pull request #700, issue #638) and that work was merged into this branch, which left `ecdb1c84` an ancestor of `HEAD` but no longer a clean pre-change baseline, because files under `QuickFiler` and `QuickFiler.Test` @@ -45,13 +47,16 @@ not own, and the gates that enumerate or scope over a whole tree — P0-T8's emp proof, P6-T6's exact ten-path enumeration, P8-T30's `QuickFiler QuickFiler.Test` diff, and P5-T5's tree-wide `QuickFiler.Test` diff — would be unsatisfiable as written. Anchoring at the merge commit puts all of that work behind the anchor. -`0eda184ca0009bc79ac9b7146897270c17c095fa` is the post-merge, pre-change baseline, and P0-T8 proves -that property by a check that can fail rather than by an ancestry check that cannot. The proof is -not vacuous merely because this anchor is the branch tip at the moment this plan was amended. -P0-T8's diff fails as soon as any file under `QuickFiler` or `QuickFiler.Test` differs between the -anchor and `HEAD` in the checkout the executor actually runs in, and its porcelain companion fails -on any staged, unstaged or untracked change in those same trees. The anchor stops being the tip -at P6-T6, after which every later anchored diff carries this plan's own changes. +`0eda184ca0009bc79ac9b7146897270c17c095fa` was the post-merge, pre-change baseline for the completed +tasks, and P0-T8 proved that property by a check that could fail rather than by an ancestry check +that could not. After those tasks completed, `main` advanced through commit +`3be3f237a8551df3f27f83d9d1af2f26074fc93a` and was merged at +`dfa6abe00d6aec18458f3fd4cc6d60d99a796173`. That merge adds main-owned `QuickFiler` and +`QuickFiler.Test` changes between the historical anchor and `HEAD`, so P8-T30 uses the merged +`main` parent as its current scope boundary. The P8-T30 diff fails as soon as a file under +`QuickFiler` or `QuickFiler.Test` differs from that parent other than the issue #637 paths it +enumerates, and its porcelain companion fails on any staged, unstaged or untracked change in the +audited trees. **Git pathspec scoping** — `.claude/` is a tracked directory in this repository and carries unrelated in-flight modifications, and `docs/features/parallel/` and `artifacts/` are owned by other processes. @@ -247,11 +252,13 @@ tree observation. `.csharpierignore` excludes `**/evidence/**`, `*.cobertura.xml `*.csproj`, `*.props` and `*.targets`, so evidence artifacts, coverage documents and the test project file are outside the formatter's scope. -**Anchored-diff form.** Before P6-T6 commits, nothing this plan changes is in `HEAD`, so a two-dot -`BASE..HEAD` diff reports nothing for it. Every pre-commit diff gate in this plan therefore uses the -index form `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- <paths>` and is preceded in -the same task by a `git add` over the same paths. Every post-commit diff gate uses -`git diff 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- <paths>`. Both forms are anchored to an +**Anchored-diff form.** Before P6-T6 committed, nothing this plan changed was in `HEAD`, so a two-dot +`BASE..HEAD` diff reported nothing for it. The completed pre-commit gates therefore used the index +form `git diff 0eda184ca0009bc79ac9b7146897270c17c095fa --cached -- <paths>` after a matching +`git add`, and the completed post-commit gates used +`git diff 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- <paths>`. The remaining post-merge +scope gate P8-T30 uses `git diff 3be3f237a8551df3f27f83d9d1af2f26074fc93a..HEAD -- <paths>` so it +excludes main-owned changes already present in that parent. Every diff form remains anchored to an explicit ref; the bare unanchored `git diff` is never used. **Name-listing diffs carry a companion.** A `git diff --name-only` or `--name-status` enumerates @@ -1115,7 +1122,12 @@ commit SHA in the canonical checkpoint. changed to `coverage\testresults\p4-t4`, and write `evidence/regression-testing/p4-t4-helper-tests-green.md`. Acceptance: `EXIT_CODE: 0`; 8 tests total; 8 passed; 0 failed; and the two tests that failed in P2-T12 are named individually as now - passing. + passing. When the preserved stdout summary does not name that required pair, preserve it unchanged + and write the factual supplemental artifact + `evidence/regression-testing/p4-t4-helper-tests-derived-summary.md`, derived only from the retained + P4-T4 TRX result XML. The supplemental artifact must name both required tests, record each `Passed` + outcome and the retained source SHA256, and use `<trx-file>` rather than a host-derived result-file + name. The original artifact and retained result XML must not be modified. - [x] [P4-T5] Prove the eight existing `ToArchiveRelativeStem` tests are unchanged and still pass. Use the P2-T11 command with the filter `"/TestCaseFilter:FullyQualifiedName~EfcDataModelIssue614Tests&TestCategory!=LiveOutlook"` and the @@ -1578,26 +1590,26 @@ Each task below verifies one acceptance criterion against evidence already on di that criterion's `- [ ]` to `- [x]` in the `## Acceptance Criteria` section of `spec.md`. No criterion is checked off before its cited evidence exists. Exactly one criterion is checked off per task. -- [ ] [P8-T1] AC1: cite `evidence/regression-testing/p3-t5-router-tests-green.md` showing +- [x] [P8-T1] AC1: cite `evidence/regression-testing/p3-t5-router-tests-green.md` showing `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected` and `RowSelected_ArchiveRootExactFilingTarget_PreservesAPriorValidSelection` passing, and `evidence/regression-testing/p2-t11-router-tests-red.md` showing both failing before the fix. Acceptance: both artifacts exist and name both tests; AC1 is checked off. -- [ ] [P8-T2] AC2: cite `evidence/regression-testing/p3-t5-router-tests-green.md` showing +- [x] [P8-T2] AC2: cite `evidence/regression-testing/p3-t5-router-tests-green.md` showing `RowSelected_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem` and `RowSelected_RootedTargetUnderArchiveRoot_CaseInsensitiveAndTrailingSeparatorRoot_CommitsTheStem` passing. Acceptance: the artifact names both tests as passing; AC2 is checked off. -- [ ] [P8-T3] AC3: cite `evidence/regression-testing/p3-t2-nesting.md` for the nesting inside the +- [x] [P8-T3] AC3: cite `evidence/regression-testing/p3-t2-nesting.md` for the nesting inside the `IsFullOutlookPath` arm, and `evidence/regression-testing/p3-t5-router-tests-green.md` for `RowSelected_RelativeFilingTarget_CommitsTheValueVerbatim` passing. Acceptance: both artifacts exist and the nesting artifact quotes the edited body; AC3 is checked off. -- [ ] [P8-T4] AC4: cite `evidence/baseline/p1-t6-passthrough-tests.md` for the two existing tests, +- [x] [P8-T4] AC4: cite `evidence/baseline/p1-t6-passthrough-tests.md` for the two existing tests, `evidence/regression-testing/p3-t6-router-siblings.md` and `evidence/regression-testing/p5-t6-issue439-green.md` for both passing unmodified, and `evidence/regression-testing/p3-t5-router-tests-green.md` for the new `RowSelected_RootedTargetWithNoBoundArchiveRoot_PassesThroughVerbatim`. Acceptance: all three artifacts exist and name the tests; AC4 is checked off. -- [ ] [P8-T5] AC5: cite `evidence/regression-testing/p3-t5-router-tests-green.md` for +- [x] [P8-T5] AC5: cite `evidence/regression-testing/p3-t5-router-tests-green.md` for `RowSelected_TrashPseudoRow_CommitsTheSentinelVerbatim`, and `evidence/regression-testing/p4-t4-helper-tests-green.md` for `ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim`. Acceptance: both artifacts exist @@ -1606,31 +1618,31 @@ is checked off before its cited evidence exists. Exactly one criterion is checke comparison is at line **316** on the merged tree — that the discrepancy is recorded in full by P8-T32, and that AC5's binding clause, which is behavioral rather than positional, is unaffected; AC5 is checked off. -- [ ] [P8-T6] AC6: cite `evidence/regression-testing/p3-t2-nesting.md` for the preserved message +- [x] [P8-T6] AC6: cite `evidence/regression-testing/p3-t2-nesting.md` for the preserved message literal, `evidence/regression-testing/p3-t5-router-tests-green.md` for `RowSelected_OutOfRootRootedTarget_IsStillRejected` and `RowSelected_SeparatorBoundaryNearMissTarget_IsStillRejected`, and `evidence/regression-testing/p3-t6-router-siblings.md` for `RowSelected_OutOfRootFilingTarget_DoesNotStoreAFullOutlookPath`. Acceptance: all three artifacts exist and name the tests; AC6 is checked off. -- [ ] [P8-T7] AC7: cite `evidence/regression-testing/p3-t2-nesting.md` recording that the new message +- [x] [P8-T7] AC7: cite `evidence/regression-testing/p3-t2-nesting.md` recording that the new message contains no `@`, and `evidence/regression-testing/p3-t5-router-tests-green.md` for `RowSelected_ArchiveRootExactFilingTarget_IsNotSelected`, which asserts through `AssertRejectionDiagnosticWithoutIdentifiers`. Acceptance: both artifacts exist; AC7 is checked off. -- [ ] [P8-T8] AC8: cite `evidence/regression-testing/p3-t3-selectionfile-diff.md` showing no hunk in +- [x] [P8-T8] AC8: cite `evidence/regression-testing/p3-t3-selectionfile-diff.md` showing no hunk in the original line range 109 to 139. Acceptance: the artifact exists and lists the hunk headers; AC8 is checked off. -- [ ] [P8-T9] AC9: re-run both P1-T1 searches against the post-change tree and write +- [x] [P8-T9] AC9: re-run both P1-T1 searches against the post-change tree and write `evidence/qa-gates/p8-t9-selection-family-post.md`. Acceptance: both searches still return exactly 9 lines with the same classification of 2 declarations and 7 call sites, and no Family-B member appears in either result; AC9 is checked off. -- [ ] [P8-T10] AC10: cite `evidence/regression-testing/p3-t5-router-tests-green.md` for +- [x] [P8-T10] AC10: cite `evidence/regression-testing/p3-t5-router-tests-green.md` for `SelectFirstRow_RootedTargetUnderArchiveRoot_CommitsTheArchiveRelativeStem`, which reaches `SelectRow` through the public `SelectFirstRow` at `BreadcrumbBridgeRouter.cs:196-203` rather than through the `rowSelected` inbound message. Acceptance: the artifact names that test as passing; AC10 is checked off. -- [ ] [P8-T11] AC11: cite `evidence/regression-testing/p4-t2-helper-shape.md` for the single +- [x] [P8-T11] AC11: cite `evidence/regression-testing/p4-t2-helper-shape.md` for the single `internal static` declaration in `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, the single assignment call site at `QuickFiler/Controllers/EfcDataModel.cs:337`, and the purity record, and `evidence/regression-testing/p4-t4-helper-tests-green.md` for the eight tests that invoke the @@ -1644,23 +1656,33 @@ is checked off before its cited evidence exists. Exactly one criterion is checke `EfcDataModel.ToFilingStemOrVerbatim` because the new file is a partial of the same type, and that the split is the remedy `spec.md:414-416` authorizes for the 15-line headroom; AC11 is checked off. -- [ ] [P8-T12] AC12: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for +- [x] [P8-T12] AC12: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for the P4-T4 + green-run headline and `evidence/regression-testing/p4-t4-helper-tests-derived-summary.md` for `ToFilingStemOrVerbatim_RelativeStem_ReturnsTheInputVerbatim` and - `ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim`. Acceptance: the artifact names - both tests as passing; AC12 is checked off. -- [ ] [P8-T13] AC13: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for + `ToFilingStemOrVerbatim_TrashSentinel_ReturnsTheInputVerbatim`. Acceptance: the supplemental + artifact names both tests as `Passed`, identifies the retained P4-T4 TRX result XML by its recorded + SHA256 without exposing the host-derived file name, and AC12 is checked off. +- [x] [P8-T13] AC13: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for `ToFilingStemOrVerbatim_RootedUnderAncestor_ReturnsTheStem` and `ToFilingStemOrVerbatim_RootedUnderCaseDifferingAncestor_ReturnsTheStem`, together with `evidence/regression-testing/p2-t12-helper-tests-red.md` showing both failing before the fix. Acceptance: both artifacts exist and name both tests; AC13 is checked off. -- [ ] [P8-T14] AC14: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for - `ToFilingStemOrVerbatim_ArchiveRootExact_ReturnsTheInputVerbatimAndDoesNotThrow`, - `ToFilingStemOrVerbatim_OutOfRootRootedInput_ReturnsTheInputVerbatimAndDoesNotThrow`, - `ToFilingStemOrVerbatim_NullEmptyWhitespaceOrSeparatorOnlyAncestor_ReturnsTheInputVerbatim` and - `ToFilingStemOrVerbatim_NullOrEmptyCandidate_ReturnsTheInputVerbatim`, and - `evidence/regression-testing/p4-t2-helper-shape.md` for the record that the body contains no - `throw`. Acceptance: both artifacts exist and name all four tests; AC14 is checked off. -- [ ] [P8-T15] AC15: cite `evidence/baseline/p1-t5-toarchiverelativestem-tests.md` for the count of 8 +- [x] [P8-T14] AC14: cite `evidence/regression-testing/p4-t4-helper-tests-green.md` for the P4-T4 + green-run headline, `evidence/regression-testing/p4-t4-helper-tests-ac14-derived-summary.md` for + the four retained-result entries, and `evidence/regression-testing/p4-t2-helper-shape.md` for the + record that the body contains no `throw`. The supplemental artifact must identify the retained + P4-T4 TRX result XML only by SHA256 + `D55969654086D9C56BD799F313CEE2A0FB9A6C17FDE0C2631050DEFE8077374C`, state that the source + hash was verified before parsing, and list these exact fully qualified result names with outcome + `Passed`: `QuickFiler.Test.Controllers.EfcDataModelIssue637Tests.ToFilingStemOrVerbatim_ArchiveRootExact_ReturnsTheInputVerbatimAndDoesNotThrow`, + `QuickFiler.Test.Controllers.EfcDataModelIssue637Tests.ToFilingStemOrVerbatim_OutOfRootRootedInput_ReturnsTheInputVerbatimAndDoesNotThrow`, + `QuickFiler.Test.Controllers.EfcDataModelIssue637Tests.ToFilingStemOrVerbatim_NullEmptyWhitespaceOrSeparatorOnlyAncestor_ReturnsTheInputVerbatim`, + and `QuickFiler.Test.Controllers.EfcDataModelIssue637Tests.ToFilingStemOrVerbatim_NullOrEmptyCandidate_ReturnsTheInputVerbatim`. + The artifact must derive each name by pairing the retained TRX `UnitTestResult` `testName` and + `outcome` attributes with its `UnitTest/TestMethod` class name; it must not modify the retained + TRX or either existing P4-T4 Markdown summary. Acceptance: all three artifacts exist; the + supplemental artifact names all four results as `Passed`; AC14 is checked off. +- [x] [P8-T15] AC15: cite `evidence/baseline/p1-t5-toarchiverelativestem-tests.md` for the count of 8 and the declaration line numbers, and `evidence/regression-testing/p4-t5-toarchiverelativestem-unchanged.md` for the zero-removed-line diff and the 8 passing results including `ToArchiveRelativeStem_ArchiveRootItself_Throws`, and @@ -1669,7 +1691,7 @@ is checked off before its cited evidence exists. Exactly one criterion is checke Acceptance: all three artifacts exist; the check-off record states that AC15 already carries `:421-448`, `:398-419` and `:407`, re-verified against the merged tree in P8-T32 list entries A5, A6 and A8; AC15 is checked off. -- [ ] [P8-T16] AC16: re-run both P1-T2 searches against the post-change tree and write +- [x] [P8-T16] AC16: re-run both P1-T2 searches against the post-change tree and write `evidence/qa-gates/p8-t16-movetofolder-family-post.md`. Acceptance: the syntax-anchored search still returns exactly **10** lines across **5** files, classified as 3 declarations and 7 call sites; the stem search still returns **23** lines across **6** files; no new overload and no @@ -1677,7 +1699,7 @@ is checked off before its cited evidence exists. Exactly one criterion is checke figures, re-verified in P8-T32 list entry A9, and that the 16-line figure retained at `spec.md:313` describes the pre-#638 tree and is recorded in P8-T32 list entry B6; AC16 is checked off. -- [ ] [P8-T17] AC17: cite `evidence/regression-testing/p4-t6-nongoals-untouched.md`. Acceptance: the +- [x] [P8-T17] AC17: cite `evidence/regression-testing/p4-t6-nongoals-untouched.md`. Acceptance: the artifact shows no hunk in the ranges 349 to 396 (`OpenOlFolderAsync` and `OpenFsFolderAsync`) or 398 to 448 (the `MAPIFolder` overload and `ToArchiveRelativeStem`), and no hunk in the protected range 271 to 297; it records that `Globals.Ol.ArchiveRootPath` occurs exactly once in the file, @@ -1692,18 +1714,18 @@ is checked off before its cited evidence exists. Exactly one criterion is checke could make it true. AC17's current clause requires instead that #638's guarded read and degrade are preserved unchanged, which is the property this plan can and does deliver; AC17 is checked off. -- [ ] [P8-T18] AC18: cite `evidence/regression-testing/p5-t5-single-assertion-change.md`, which +- [x] [P8-T18] AC18: cite `evidence/regression-testing/p5-t5-single-assertion-change.md`, which records all three clauses. Acceptance: the artifact records the corrected assertion at line 165, the renamed method, and the narrowed two-line comment; AC18 is checked off. -- [ ] [P8-T19] AC19: cite `evidence/regression-testing/p5-t4-provider-assertion-preserved.md`. +- [x] [P8-T19] AC19: cite `evidence/regression-testing/p5-t4-provider-assertion-preserved.md`. Acceptance: the artifact quotes lines 161 to 164 byte-identically and shows no diff hunk over `ToHierarchyPath` at the original `BreadcrumbBridgeRouter.cs:152-167`; AC19 is checked off. -- [ ] [P8-T20] AC20: cite `evidence/baseline/p1-t8-pinning-assertion.md` for the pre-change derivation +- [x] [P8-T20] AC20: cite `evidence/baseline/p1-t8-pinning-assertion.md` for the pre-change derivation by two independent constructions, and `evidence/regression-testing/p5-t5-single-assertion-change.md` for the post-change diff showing exactly one removed `.Should()` line across the whole `QuickFiler.Test` tree. Acceptance: both artifacts exist and agree that the count is 1; AC20 is checked off. -- [ ] [P8-T21] AC21: write `evidence/other/p8-t21-spec-correction-record.md` carrying the change +- [x] [P8-T21] AC21: write `evidence/other/p8-t21-spec-correction-record.md` carrying the change description text for this correction, copied from the record P5-T5 wrote into `evidence/regression-testing/p5-t5-single-assertion-change.md`. This artifact is the designated source text for the pull-request change description, so the statement is owned by this plan @@ -1712,19 +1734,19 @@ is checked off before its cited evidence exists. Exactly one criterion is checke archive-relative-stem invariant, which #614 enforced on the `SelectHierarchyPath` half and at the filing boundary but not on the `SelectRow` half, and that the change is a deliberate spec correction and explicitly not a weakened test; AC21 is checked off. -- [ ] [P8-T22] AC22: cite `evidence/baseline/p1-t4-deferral-records.md` for the pre-change count of 3 +- [x] [P8-T22] AC22: cite `evidence/baseline/p1-t4-deferral-records.md` for the pre-change count of 3 and `evidence/regression-testing/p6-t4-deferral-cleared.md` for the post-change count of 0 over `*.cs`. Acceptance: both artifacts exist and the post-change count is 0; AC22 is checked off. -- [ ] [P8-T23] AC23: cite `evidence/regression-testing/p6-t4-deferral-cleared.md` for the scoped +- [x] [P8-T23] AC23: cite `evidence/regression-testing/p6-t4-deferral-cleared.md` for the scoped `EfcSelectionGuardTests` run with 0 failures including `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary`, and for the single-line diff over `EfcSelectionGuard.cs`. Acceptance: the artifact exists and records both; AC23 is checked off. -- [ ] [P8-T24] AC24: re-run both P1-T3 searches against the post-change tree and write +- [x] [P8-T24] AC24: re-run both P1-T3 searches against the post-change tree and write `evidence/qa-gates/p8-t24-selectedfolderpath-post.md`. Acceptance: the production surface is still 9 lines across 3 files with 2 writes and 3 reads, no new write site appears, no new public API member appears, and `rg -n "public string\? SelectedFolderPath \{ get; private set; \}" QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` returns exactly 1 line; AC24 is checked off. -- [ ] [P8-T25] AC25: cite `evidence/qa-gates/p7-t9-file-sizes.md` and +- [x] [P8-T25] AC25: cite `evidence/qa-gates/p7-t9-file-sizes.md` and `evidence/baseline/p1-t7-file-line-counts.md`. Acceptance: the artifact shows every listed file at or under 500 lines, including the new `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`; `BreadcrumbBridgeRouterIssue439Tests.cs` at @@ -1736,17 +1758,17 @@ is checked off before its cited evidence exists. Exactly one criterion is checke names the new file; AC25 is checked off. The bound is stated as "at or under" rather than "exactly" for the same reason it is in P7-T9: the figure is read after a write-mode formatter that can reduce a line count, and AC25 requires only non-growth. -- [ ] [P8-T26] AC26: cite `evidence/regression-testing/p2-t13-compile-include-observed.md` for the +- [x] [P8-T26] AC26: cite `evidence/regression-testing/p2-t13-compile-include-observed.md` for the `Compile Include` line and the 10 observed test results, and `evidence/regression-testing/p3-t5-router-tests-green.md` for the same 10 tests executing after the fix. Acceptance: both artifacts exist and both record 10 executed tests; AC26 is checked off. -- [ ] [P8-T27] AC27: cite `evidence/qa-gates/p7-t4-msbuild-nullable.md` for the clean nullable build, +- [x] [P8-T27] AC27: cite `evidence/qa-gates/p7-t4-msbuild-nullable.md` for the clean nullable build, and verify in the same task that `rg -n "^#nullable enable" QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` returns line 1 and that `evidence/regression-testing/p3-t2-nesting.md` records that `stem` is a non-nullable `string` passed to `CommitSelection` without a nullable temporary. Acceptance: all three checks hold; AC27 is checked off. -- [ ] [P8-T28] AC28: cite `evidence/qa-gates/p7-t10-toolchain-audit.md`. Acceptance: the artifact +- [x] [P8-T28] AC28: cite `evidence/qa-gates/p7-t10-toolchain-audit.md`. Acceptance: the artifact quotes all four final-QC commands verbatim in order, shows `/t:Rebuild` on both MSBuild lines, records `NULLABLE_OPT_IN_PROPERTY: absent` for every one of the four quoted final-QC `Command:` lines — do not spell the token in this task's own record — and shows every step recording @@ -1767,7 +1789,7 @@ is checked off before its cited evidence exists. Exactly one criterion is checke repository's standard runner and the local analogue of `.github/workflows/_mstest-coverage.yml:83`. The substitutions are recorded, not resolved. AC28 is checked off. -- [ ] [P8-T29] AC29: cite `evidence/baseline/p0-t16-coverage-headline.md`, +- [x] [P8-T29] AC29: cite `evidence/baseline/p0-t16-coverage-headline.md`, `evidence/qa-gates/p7-t6-coverage-headline.md`, `evidence/qa-gates/p7-t7-changed-line-coverage.md` and `evidence/qa-gates/p7-t8-coverage-delta.md`. Acceptance: the baseline capture is under @@ -1791,19 +1813,21 @@ is checked off before its cited evidence exists. Exactly one criterion is checke shows both branches taken, per the `condition-coverage` values P7-T7 recorded, or — when P7-T7 records that the helper's range carries no `branch="True"` node — per the two witness tests P7-T7 names; and AC29 is checked off. -- [ ] [P8-T30] AC30: verify no behavior outside changes A through D was altered. Run three commands in +- [x] [P8-T30] AC30: verify no behavior outside changes A through D was altered after the merge from + `main`. Run three commands in this task, in this order. First the porcelain companion, `git status --porcelain -- QuickFiler QuickFiler.Test UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. Second - `git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- QuickFiler QuickFiler.Test`. + `git diff --name-only 3be3f237a8551df3f27f83d9d1af2f26074fc93a..HEAD -- QuickFiler QuickFiler.Test`. Third - `git diff --name-only 0eda184ca0009bc79ac9b7146897270c17c095fa..HEAD -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. + `git diff --name-only 3be3f237a8551df3f27f83d9d1af2f26074fc93a..HEAD -- UtilitiesCS UtilitiesCS.Test TaskMaster TaskMaster.Test ToDoModel Tags TaskVisualization`. Write `evidence/qa-gates/p8-t30-scope-boundary.md` recording all three outputs verbatim. The porcelain companion is required because a name-listing diff enumerates tracked changes only and never reports an untracked path, so the two diffs alone cannot fail on a file this plan created and left uncommitted. At the point this task runs the division of labour between the two mechanisms is fixed and is stated here: P6-T6 committed changes A through D, P7-T2 committed the - formatting result, and P7-T12 committed the Phase 7 evidence, so both anchored diffs carry the + formatting result, P7-T12 committed the Phase 7 evidence, and the `main` parent named above + excludes the subsequent main-owned changes, so both anchored diffs carry the issue #637-only enumeration assertion, and the porcelain span is expected to be empty because every path it covers is already in `HEAD`. That emptiness is itself the assertion and not a null result — an untracked or unstaged file anywhere in those @@ -1817,11 +1841,13 @@ is checked off before its cited evidence exists. Exactly one criterion is checke `BASELINE_FORMAT_DRIFT` section of `evidence/baseline/p0-t12-csharpier-check.md` is non-empty — the paths in that section that lie under `QuickFiler` or `QuickFiler.Test`, each of which the artifact must show as a formatting-only change committed by P7-T2, and no others; when - `BASELINE_FORMAT_DRIFT` is empty the list is exactly the ten paths; the third command + `BASELINE_FORMAT_DRIFT` is empty the list is exactly the ten paths. Files introduced by the + merged `main` commit do not appear because they are present in the post-merge parent; their + appearance is neither an issue #637 change nor a P8-T30 failure. The third command produces no output, which is the evidence that `UtilitiesCS`, `TaskMaster`, `ToDoModel`, `Tags`, `TaskVisualization`, `UtilitiesCS.Test` and `TaskMaster.Test` contain no changed file; and AC30 is checked off. -- [ ] [P8-T31] Verify all thirty criteria are checked off, using two independently constructed +- [x] [P8-T31] Verify all thirty criteria are checked off, using two independently constructed section-scoped counts. Construction 1, range-scoped: extract the lines of `spec.md` between the line matching `^## Acceptance Criteria$` and the line matching `^## Risks & Mitigations$`, and count within that slice the lines matching `^- \[x\] AC` and the lines matching `^- \[ \] AC`. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md index 0595b3cfa..8d3e7a8a3 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md @@ -850,79 +850,79 @@ under "Corrections to the research file". ### Change A — producer normalization in `SelectRow` -- [ ] AC1. Selecting a row whose `FilingTarget` is a full Outlook path **exactly equal** to the +- [x] AC1. Selecting a row whose `FilingTarget` is a full Outlook path **exactly equal** to the bound archive root is a deterministic non-selection: `SelectRow` returns early, `SelectedFolderPath` is not written, `SelectedFolderPathChanged` is not raised, and any prior valid selection survives unchanged. Verified by a named test in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`. -- [ ] AC2. Selecting a row whose `FilingTarget` is a full Outlook path **strictly under** the bound +- [x] AC2. Selecting a row whose `FilingTarget` is a full Outlook path **strictly under** the bound archive root commits the archive-relative stem, not the rooted input — including when the root differs in case and when the root carries a trailing separator. Verified by named tests in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs`. -- [ ] AC3. The new behavior is nested inside the existing +- [x] AC3. The new behavior is nested inside the existing `ArchiveStemContract.IsFullOutlookPath(selection)` arm of the guard in `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`. An ordinary **relative** suggestion target is still committed byte-identically, proven by a test that would fail under a method-wide "commit only when `TryMakeArchiveRelative` succeeds" rewrite. -- [ ] AC4. The no-bound-root pass-through mode is preserved: with `_boundRoot.Length == 0` every +- [x] AC4. The no-bound-root pass-through mode is preserved: with `_boundRoot.Length == 0` every value, including a rooted one, is committed verbatim. The **2** existing tests that pin this pass through unmodified — `BreadcrumbBridgeRouterIssue439Tests.cs:665` (bound with `@"\"` at :645) and BreadcrumbBridgeRouterIssue614Tests.cs:221 (3-argument bind at :213) — and a new test covers the `SelectRow` path in the same mode. -- [ ] AC5. The `Trash to Delete` pseudo-row still commits `BreadcrumbRowBuilder.TrashRowText` +- [x] AC5. The `Trash to Delete` pseudo-row still commits `BreadcrumbRowBuilder.TrashRowText` byte-identically, and `EfcDataModel.MoveToFolderAsync`'s `folderpath != "Trash to Delete"` comparison at `EfcDataModel.cs:272` continues to take the same branch as before. -- [ ] AC6. Out-of-root rejection is unchanged: a rooted target outside the bound root, and a +- [x] AC6. Out-of-root rejection is unchanged: a rooted target outside the bound root, and a separator-boundary near miss such as `\Archive2\Clients`, are still rejected with the existing message `"Breadcrumb row rejected: target is outside the archive root."`, leave the prior selection untouched, and raise no event. `RowSelected_OutOfRootFilingTarget_DoesNotStoreAFullOutlookPath` (BreadcrumbBridgeRouterIssue614Tests.cs:169) passes unmodified. -- [ ] AC7. The archive-root-exact non-selection emits a **value-free** diagnostic that embeds +- [x] AC7. The archive-root-exact non-selection emits a **value-free** diagnostic that embeds neither the selection nor the archive root, asserted with the existing `AssertRejectionDiagnosticWithoutIdentifiers` helper shape (no message containing the queried fragment contains `@`). -- [ ] AC8. `SelectHierarchyPath` (`BreadcrumbBridgeRouter.Selection.cs:109-129`) and +- [x] AC8. `SelectHierarchyPath` (`BreadcrumbBridgeRouter.Selection.cs:109-129`) and `CommitSelection` (:131-139) are not modified. -- [ ] AC9. The selection family is unchanged in shape: still exactly **2** declarations +- [x] AC9. The selection family is unchanged in shape: still exactly **2** declarations (`BreadcrumbBridgeRouter.Selection.cs:83` and `:109`) and **7** call sites (4 to `SelectRow` at BreadcrumbBridgeRouter.cs:201, :286 and BreadcrumbBridgeRouter.Arrows.cs:153, :161; 3 to `SelectHierarchyPath` at `BreadcrumbBridgeRouter.Selection.cs:33`, `:47` and BreadcrumbBridgeRouter.Arrows.cs:138). No new declaration, no overload, no interface member, and no new call site is introduced, and no member of the unrelated Family-B `SelectRow(int)` surface is touched. -- [ ] AC10. The normalization is on the shared implementation, so all four `SelectRow` call sites +- [x] AC10. The normalization is on the shared implementation, so all four `SelectRow` call sites observe it. Proven by at least one regression test that reaches `SelectRow` through `SelectFirstRow()` rather than through the `rowSelected` inbound message. ### Change B — normalization in the `string` overload of `MoveToFolderAsync` -- [ ] AC11. `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, a new partial-class file of +- [x] AC11. `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, a new partial-class file of `EfcDataModel`, declares exactly one new `internal static` helper that takes the candidate path and the archive ancestor and returns the value to assign, and the `DestinationOlStem` assignment in the `string` overload (currently `EfcDataModel.cs:337`) calls it. The member is therefore `EfcDataModel.ToFilingStemOrVerbatim`, unchanged by the file split. The helper is pure: no I/O, no logging, no static mutable state, and it is invoked directly by unit tests without constructing an `EmailFiler`. -- [ ] AC12. The helper is gated on `ArchiveStemContract.IsFullOutlookPath`. Any value that is not a +- [x] AC12. The helper is gated on `ArchiveStemContract.IsFullOutlookPath`. Any value that is not a full Outlook path — every ordinary relative stem and the `"Trash to Delete"` sentinel — is returned **verbatim and byte-identical**, asserted by named tests. -- [ ] AC13. For a rooted value at or strictly under the ancestor, the helper returns the +- [x] AC13. For a rooted value at or strictly under the ancestor, the helper returns the archive-relative stem. -- [ ] AC14. The helper is **total and never throws**, for any input including archive-root-exact, +- [x] AC14. The helper is **total and never throws**, for any input including archive-root-exact, out-of-root, cross-store, null/empty candidate, and null/empty/whitespace/separator-only ancestor; in each of those cases it returns the input verbatim so the existing boundary guard decides exactly as it does today. This is a deliberate divergence from `EfcDataModel.ToArchiveRelativeStem`, which throws on the archive-root-exact input; the rationale is recorded under "Error handling and logging updates". -- [ ] AC15. `EfcDataModel.ToArchiveRelativeStem` (`EfcDataModel.cs:421-448`, declaration at `:434`), +- [x] AC15. `EfcDataModel.ToArchiveRelativeStem` (`EfcDataModel.cs:421-448`, declaration at `:434`), the `MAPIFolder` overload (`:398-419`), and its call to `ToArchiveRelativeStem` at `:407` are unmodified, and the **8** existing `ToArchiveRelativeStem` tests in `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs` (methods at :21, :34, :48, :62, :72, :87, :100, :111) pass unchanged — including `ToArchiveRelativeStem_ArchiveRootItself_Throws`. -- [ ] AC16. The `MoveToFolder` family is unchanged in shape apart from the helper call: still +- [x] AC16. The `MoveToFolder` family is unchanged in shape apart from the helper call: still **3** declarations (`EfcDataModel.cs:303`, `EfcDataModel.cs:398`, and the same-named forwarder at EfcHomeController.ExecuteMoves.cs:89) and **7** call sites (EfcHomeController.ExecuteMoves.cs:78 and :98, `EfcDataModel.cs:408`, EfcFormController.cs:537 and :844, @@ -930,7 +930,7 @@ under "Corrections to the research file". QuickFiler.Test/Controllers/EfcDataModelArchiveRootTests.cs:314). The family-stem search over `*.cs` returns **23** lines across **6** files, and the syntax-anchored search returns **10** lines across **5** files. No new overload and no signature change. -- [ ] AC17. `EfcDataModel.OpenOlFolderAsync` (`:349-372`) and `OpenFsFolderAsync` (`:374-396`) are +- [x] AC17. `EfcDataModel.OpenOlFolderAsync` (`:349-372`) and `OpenFsFolderAsync` (`:374-396`) are **not** modified, and the guarded `Globals.Ol.ArchiveRootPath` read at `EfcDataModel.cs:284` together with the `UserDiagnosticAction(ArchiveRootUnavailableMessage)` degrade at `:358` and `:382`, both introduced by issue #638, are preserved unchanged. The remaining benign-degrade @@ -938,42 +938,42 @@ under "Corrections to the research file". ### Change C — test spec correction -- [ ] AC18. `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs:165` asserts +- [x] AC18. `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs:165` asserts `router.SelectedFolderPath.Should().Be(@"Clients\North");`, the enclosing test method is renamed so it no longer asserts that a rooted target "RemainsUnchanged", and the arrange comment at :121-122 is narrowed to the provider claim it still supports. -- [ ] AC19. The companion assertion at `BreadcrumbBridgeRouterIssue439Tests.cs:161-164` +- [x] AC19. The companion assertion at `BreadcrumbBridgeRouterIssue439Tests.cs:161-164` (`provider.Verify(p => p.ResolveLeafKeyAsync(fullTarget, ...), Times.Once)`) is preserved verbatim, and `ToHierarchyPath` (BreadcrumbBridgeRouter.cs:152-167) is unmodified, so the provider lookup still uses the original rooted path. -- [ ] AC20. Exactly **1** existing test assertion changes its expected value across the entire +- [x] AC20. Exactly **1** existing test assertion changes its expected value across the entire repository — the one at `BreadcrumbBridgeRouterIssue439Tests.cs:165`. No other existing assertion in any test project is modified, weakened, disabled, or deleted. -- [ ] AC21. The change is recorded in the change description as a **deliberate spec correction**: +- [x] AC21. The change is recorded in the change description as a **deliberate spec correction**: the issue #439 criterion that a rooted target survives selection is superseded by issue #614's archive-relative-stem invariant, which #614 enforced on the `SelectHierarchyPath` half and at the filing boundary but not on the `SelectRow` half. It is explicitly not a weakened test. ### Change D — stale-comment cleanup -- [ ] AC22. All **3** stale deferral records are corrected to state that producer-side normalization +- [x] AC22. All **3** stale deferral records are corrected to state that producer-side normalization is implemented: `QuickFiler/Controllers/EfcSelectionGuard.cs:30`, `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:146`, and the `because` string at `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs:152`. After the change, a repository grep for `deferred to issue #637` across `*.cs` returns **0** matches. -- [ ] AC23. `EfcSelectionGuard` behavior is unchanged — `IsValidFilingSelection` and +- [x] AC23. `EfcSelectionGuard` behavior is unchanged — `IsValidFilingSelection` and `IsValidCreationSelection` still reject rooted values — and every test in `QuickFiler.Test/Controllers/EfcSelectionGuardTests.cs` passes, including the composition test `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` (:167-213). ### Cross-cutting -- [ ] AC24. The `SelectedFolderPath` production surface is unchanged in shape: still **9** lines +- [x] AC24. The `SelectedFolderPath` production surface is unchanged in shape: still **9** lines across **3** files, with **2** write sites (`BreadcrumbBridgeRouter.Selection.cs:134`, BreadcrumbBridgeRouter.cs:145) and **3** read sites (BreadcrumbBridgeRouter.cs:143, `BreadcrumbBridgeRouter.Selection.cs:138`, EfcFormController.cs:321). No new write site, no new public API member, and the property's `private set` is preserved. -- [ ] AC25. File-size limits hold. `QuickFiler/Controllers/EfcDataModel.cs` remains at or under 500 +- [x] AC25. File-size limits hold. `QuickFiler/Controllers/EfcDataModel.cs` remains at or under 500 lines (485 before the change, leaving 15 lines of headroom, which is why the change-B helper is declared in its own partial-class file as this document's implementation section authorizes); `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` does not grow beyond its @@ -981,28 +981,28 @@ under "Corrections to the research file". `QuickFiler/Controllers/EfcDataModel.FilingStem.cs`, `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue637Tests.cs` and `QuickFiler.Test/Controllers/EfcDataModelIssue614Tests.cs`, is at or under 500 lines. -- [ ] AC26. `QuickFiler.Test/QuickFiler.Test.csproj` contains a `<Compile Include>` item for +- [x] AC26. `QuickFiler.Test/QuickFiler.Test.csproj` contains a `<Compile Include>` item for `Controllers\BreadcrumbBridgeRouterIssue637Tests.cs`, and the new tests are observed executing in the vstest run output (a test file absent from this non-SDK project compiles into nothing and silently never runs). -- [ ] AC27. Nullable posture is respected: `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` +- [x] AC27. Nullable posture is respected: `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` keeps its `#nullable enable` directive, the edited lines introduce no `CS86xx` diagnostic under `/p:TreatWarningsAsErrors=true`, and no nullable temporary is passed to `CommitSelection`, whose parameter is a non-nullable `string`. -- [ ] AC28. Full C# toolchain pass completed in order with no failures in the final pass, using +- [x] AC28. Full C# toolchain pass completed in order with no failures in the final pass, using exactly these commands: `dotnet tool run csharpier format .` (verified with `dotnet tool run csharpier check .`); `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`; `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`; `vstest.console.exe <test-assembly-paths> /EnableCodeCoverage`. `/p:Nullable=enable` was not added and `/t:Build` was not substituted. -- [ ] AC29. Coverage evidence is captured under the canonical evidence kinds — the pre-change +- [x] AC29. Coverage evidence is captured under the canonical evidence kinds — the pre-change capture under `<FEATURE>/evidence/baseline/` and the post-change capture under `<FEATURE>/evidence/qa-gates/`, per `evidence-and-timestamp-conventions`: repository line coverage is at or above the policy floor, no changed line loses coverage relative to the base commit, and the new change-B helper meets the new-code coverage target with both sides of its gate exercised. -- [ ] AC30. No behavior outside changes A-D is altered. Specifically unchanged: +- [x] AC30. No behavior outside changes A-D is altered. Specifically unchanged: `ArchiveStemContract`, `EmailFilerConfig.ResolvePaths` / `RequireArchiveRelativeStem`, `EfcHomeController.ExecuteMovesAsync`, EfcFormController.cs, the Family-B breadcrumb surface, and all `UtilitiesCS.Test` suites. From a314228b9c3d9a4944a9e88e1a4eb4bd9c4b0f7b Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 12:25:51 -0400 Subject: [PATCH 14/16] docs(issue-637): reconcile final plan evidence - Record spec-to-tree citation discrepancies without changing acceptance criteria - Preserve AC21 as a deliberate invariant correction - Mark final partial-interval plan tasks complete Refs: #637 --- .../other/p8-t32-spec-tree-discrepancies.md | 38 +++++++++++++++++++ .../evidence/other/p8-t33-final-commit.md | 24 ++++++++++++ .../plan.2026-08-29T12-20.md | 4 +- 3 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t32-spec-tree-discrepancies.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t33-final-commit.md diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t32-spec-tree-discrepancies.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t32-spec-tree-discrepancies.md new file mode 100644 index 000000000..f4351fbeb --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t32-spec-tree-discrepancies.md @@ -0,0 +1,38 @@ +Timestamp: 2026-08-31T12:23:19-04:00 +Scope: P8-T32 spec-versus-tree reconciliation after the issue #638 merge +Verification basis: direct line-count and symbol searches of the merged working tree, plus the retained P1-T2 and P8-T16 census evidence. + +This task does not modify any `spec.md` text. Its only earlier changes to `spec.md` were the P8-T1 through P8-T30 checkbox flips. The acceptance-criteria count remains 30; no criterion was added, removed, split, or text-edited. + +## List A — citations already corrected before this execution + +Each entry below is a re-verification, not a correction made by this plan. + +| Entry | Spec figure | Merged-tree measurement | Agreement | Binding AC clause affected | +|---|---|---|---|---| +| A1 / AC25 (`spec.md:976-983`) | `EfcDataModel.cs` is 485 lines with 15 lines of headroom | 485 lines | Yes | No; the verified value explains the required change-B file split. | +| A2 / AC16 (`spec.md:926`) | `string` overload declaration at `EfcDataModel.cs:303` | declaration at 303 | Yes | No. | +| A3 / AC11 (`spec.md:900-906`) | `EfcDataModel.FilingStem.cs`; `DestinationOlStem` assignment at `EfcDataModel.cs:337` | assignment at 337 | Yes | No. | +| A4 / AC17 (`spec.md:933`) | `OpenOlFolderAsync` at 349-372 and `OpenFsFolderAsync` at 374-396 | 349-372 and 374-396 | Yes | No. | +| A5 / AC15 and AC16 (`spec.md:918-920`, `:926`) | `MAPIFolder` overload at 398-419; declaration at 398 | 398-419; declaration at 398 | Yes | No. | +| A6 / AC15 (`spec.md:920`) | `ToArchiveRelativeStem` call at 407 | call at 407 | Yes | No. | +| A7 / AC16 (`spec.md:928`) | delegation call at 408 | call at 408, spanning 408-414 | Yes | No. | +| A8 / AC15 (`spec.md:918`) | `ToArchiveRelativeStem` at 421-448; declaration at 434 | 421-448; declaration at 434 | Yes | No. | +| A9 / AC16 (`spec.md:925-932`) | 3 declarations and 7 call sites; family stem 23 lines across 6 files; syntax search 10 lines across 5 files | retained P1-T2 and P8-T16 evidence measures exactly those figures | Yes | No. | +| A10 / AC17 (`spec.md:933-937`) | guarded `Globals.Ol.ArchiveRootPath` read at 284 and `UserDiagnosticAction(ArchiveRootUnavailableMessage)` degradation at 358 and 382 are preserved | read at 284; degradations at 358 and 382 | Yes | No. The prior wording asserting that no archive-root read gained guarding/degradation was already false after #638, so the current satisfiable preservation clause remains required. | + +## List B — stale citations deliberately left uncorrected + +Each discrepancy is either satisfiable as written or outside the acceptance criteria. No binding acceptance-criterion clause is changed. + +| Entry | Spec figure | Merged-tree measurement | Agreement | Binding AC clause affected | +|---|---|---|---|---| +| B1 / AC23 (`spec.md:967`; prose `:783`) | `Issue614_GuardAcceptedSelection_DoesNotThrowAtFilingBoundary` at 167-213 | method spans 167-214 (declaration at 168) | No | No; AC23 requires guard behavior and passing tests, not the endpoint citation. | +| B2 (`spec.md:376`) | #499 clear-on-rebind block at `BreadcrumbBridgeRouter.cs:143-146` | block spans 143-147; AC24's read at 143 and write at 145 are exact | No | No. | +| B3 / AC5 (`spec.md:874`; prose `:127`, `:360`, `:635`, `:1021`) | `folderpath != "Trash to Delete"` comparison at `EfcDataModel.cs:272` | comparison at 316 | No | No; AC5 is behavioral. | +| B4 (implementation/headroom/census prose at `spec.md:401`, `:414-416`, `:582`, `:710`) | 424 lines and 76 lines of headroom | 485 lines and 15 lines of headroom | No | No; the change-B split follows the measured value. | +| B5 (prose `spec.md:119-120`, `:122`, `:284`, `:445`, `:469`, `:514`) | pre-#638 string-overload declaration at 259-265 and assignment at 287 | declaration at 303-309; assignment at 337 | No | No. | +| B6 (`spec.md:313`) | `MoveToFolder` family: 16 lines across 5 files | 23 stem lines across 6 files | No | No; the sentence explicitly describes the pre-#638 research correction. | +| B7 (`spec.md:164-172`) | `Globals.Ol.ArchiveRootPath` benign-degrade item remains a pending #695 non-goal; verbatim `DestinationOlStem` assignments at 308 and 326 | EfcDataModel half shipped in #638; verbatim assignments remain at 364 and 388 | No | No; this is non-AC prose, while the assignment-value preservation still holds. | + +AC21 remains recorded as a deliberate spec correction in `evidence/other/p8-t21-spec-correction-record.md`: the superseding issue #614 archive-relative-stem invariant had been applied to `SelectHierarchyPath` and the filing boundary but not to `SelectRow`. It is not a weakened test. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t33-final-commit.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t33-final-commit.md new file mode 100644 index 000000000..0bd9e0f2b --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t33-final-commit.md @@ -0,0 +1,24 @@ +Timestamp: 2026-08-31T12:23:56-04:00 +Command: `git status --porcelain -- QuickFiler QuickFiler.Test` +EXIT_CODE: 0 +Output Summary: The production and test pathspec is clean. + +Production porcelain output: + +``` +(no output) +``` + +Command: `git status --porcelain -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` +EXIT_CODE: 0 +Output Summary: The feature-folder pathspec contains only this interval's plan update and P8-T32/P8-T33 evidence artifacts; all other feature-folder paths are already in `HEAD`. + +Feature-folder porcelain output: + +``` + M docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +?? docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t32-spec-tree-discrepancies.md +?? docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p8-t33-final-commit.md +``` + +No `git commit` was invoked. The next orchestration transition must stage only this feature folder, obtain canonical commit context and a routed commit-steward message, create the final partial-interval commit, record its SHA, and then verify both pathspec spans are empty. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md index a162ca85f..7817da6ed 100644 --- a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/plan.2026-08-29T12-20.md @@ -1864,7 +1864,7 @@ is checked off before its cited evidence exists. Exactly one criterion is checke canonical commit context and the routed commit-steward profile, then recorded the boundary SHA and the P7-T12 intermediate SHA within this task interval in the canonical checkpoint. -- [ ] [P8-T32] Record the spec-versus-tree reconciliation in +- [x] [P8-T32] Record the spec-versus-tree reconciliation in `evidence/other/p8-t32-spec-tree-discrepancies.md`. `spec.md` was authored against the tree before issue #638 merged, and a prior revision of `spec.md` already applied the acceptance- criteria corrections that shift required. This plan performs no `spec.md` text edit: its only @@ -1937,7 +1937,7 @@ P7-T12 intermediate SHA within this task interval in the canonical checkpoint. The acceptance-criteria count in `spec.md` is unchanged at 30: this plan adds, removes and splits no criterion, and edits no criterion's text. -- [ ] [P8-T33] Finalise the last partial interval without invoking `git commit`. Write +- [x] [P8-T33] Finalise the last partial interval without invoking `git commit`. Write `evidence/other/p8-t33-final-commit.md`, check off this task, verify that `git status --porcelain -- QuickFiler QuickFiler.Test` produces no output, record `git status --porcelain -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637` From 952a760fb19ff9c10007fe2ebb42f8cadd49a886 Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 13:19:33 -0400 Subject: [PATCH 15/16] test(breadcrumb): split Issue 439 activation fixture - Keep the test fixture below the file-size policy and include the partial file - Record bounded coverage recovery and passing C# QA evidence - Capture remediation review and scope-validation artifacts Refs: #637, #439 --- ...umbBridgeRouterIssue439Tests.Activation.cs | 253 ++++++++++++++++++ .../BreadcrumbBridgeRouterIssue439Tests.cs | 241 +---------------- QuickFiler.Test/QuickFiler.Test.csproj | 3 +- .../code-review.2026-08-31T13-32.md | 29 ++ .../other/p1-t1-issue439-split-map.md | 21 ++ .../other/p3-t1-remediation-scope-audit.md | 35 +++ .../other/p3-t2-remediation-commit-handoff.md | 47 ++++ .../p1-t5-fixture-split-verification.md | 34 +++ .../qa-gates/p2-t1-csharpier-format.md | 9 + .../qa-gates/p2-t2-csharpier-check.md | 9 + .../qa-gates/p2-t3-msbuild-analyzers.md | 9 + .../qa-gates/p2-t4-msbuild-nullable.md | 9 + .../qa-gates/p2-t5-mstest-coverage.md | 11 + .../qa-gates/p2-t6-coverage-comparison.md | 23 ++ .../qa-gates/p2-t7-remediation-qa-audit.md | 15 ++ .../remediation-baseline/p0-t1-policy-read.md | 24 ++ .../p0-t2-csharpier-check.md | 9 + .../p0-t3-msbuild-analyzers.md | 9 + .../p0-t4-msbuild-nullable.md | 9 + .../p0-t5-mstest-coverage-retry.md | 13 + .../p0-t5-mstest-coverage.md | 11 + .../p0-t6-issue439-fixture-inventory.md | 32 +++ .../feature-audit.2026-08-31T13-32.md | 40 +++ .../policy-audit.2026-08-31T13-32.md | 108 ++++++++ .../remediation-inputs.2026-08-31T13-32.md | 41 +++ .../remediation-plan.2026-08-31T13-33.md | 47 ++++ 26 files changed, 850 insertions(+), 241 deletions(-) create mode 100644 QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-32.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p1-t1-issue439-split-map.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t1-remediation-scope-audit.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t2-remediation-commit-handoff.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p1-t5-fixture-split-verification.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t1-csharpier-format.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t2-csharpier-check.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t3-msbuild-analyzers.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t4-msbuild-nullable.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t5-mstest-coverage.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t6-coverage-comparison.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t7-remediation-qa-audit.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t1-policy-read.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t2-csharpier-check.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t3-msbuild-analyzers.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t4-msbuild-nullable.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage-retry.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t6-issue439-fixture-inventory.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-32.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-32.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-inputs.2026-08-31T13-32.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-plan.2026-08-31T13-33.md diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs new file mode 100644 index 000000000..6decfe9af --- /dev/null +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs @@ -0,0 +1,253 @@ +using System.Threading; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using QuickFiler.Controllers; +using QuickFiler.Viewers; +using UtilitiesCS; +using UtilitiesCS.OutlookObjects.Folder; + +namespace QuickFiler.Test.Controllers +{ + public partial class BreadcrumbBridgeRouterIssue439Tests + { + [TestMethod] + public void Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget() + { + const string archiveRootPath = @"\\mailbox@example.com\Archive"; + const string presentedTarget = @"Clients\North"; + const string hierarchyPath = @"\\mailbox@example.com\Archive\Clients\North"; + FolderTreeNodeKey key = Key(hierarchyPath); + var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); + var host = new Mock<IBreadcrumbWebHost>(MockBehavior.Strict); + host.SetupGet(value => value.IsCoreInitialized).Returns(true); + host.Setup(value => value.NavigateToString(It.IsAny<string>())); + host.Setup(value => value.PostMessageJson(It.IsAny<string>())); + provider + .Setup(value => + value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()) + ) + .ReturnsAsync(key); + provider + .Setup(value => value.GetAncestorChainAsync(key, It.IsAny<CancellationToken>())) + .ReturnsAsync(Chain(hierarchyPath, "Clients", "North")); + var router = new BreadcrumbBridgeRouter( + provider.Object, + host.Object, + new BreadcrumbMessageCodec(), + new BreadcrumbHtmlRenderer(), + new BreadcrumbOutboundQueue(host.Object) + ); + + router + .BindRowsAsync( + new[] { presentedTarget }, + new[] { new FolderScore(presentedTarget, 730, 0.73) }, + archiveRootPath, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync("{\"type\":\"rowSelected\",\"rowId\":\"row-0\"}") + .GetAwaiter() + .GetResult(); + + provider.Verify( + value => value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()), + Times.Once + ); + router.SelectedFolderPath.Should().Be(presentedTarget).And.NotBe(hierarchyPath); + } + + [TestMethod] + public void Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget() + { + const string archiveRootPath = @"\\mailbox@example.com\Archive"; + const string presentedTarget = @"Clients\North"; + const string hierarchyPath = @"\\mailbox@example.com\Archive\Clients\North"; + FolderTreeNodeKey leafKey = Key(hierarchyPath); + var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); + var host = new Mock<IBreadcrumbWebHost>(MockBehavior.Strict); + host.SetupGet(value => value.IsCoreInitialized).Returns(true); + host.Setup(value => value.NavigateToString(It.IsAny<string>())); + host.Setup(value => value.PostMessageJson(It.IsAny<string>())); + provider + .Setup(value => + value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()) + ) + .ReturnsAsync(leafKey); + provider + .Setup(value => value.GetAncestorChainAsync(leafKey, It.IsAny<CancellationToken>())) + .ReturnsAsync(Chain(hierarchyPath, "Clients", "North")); + var router = new BreadcrumbBridgeRouter( + provider.Object, + host.Object, + new BreadcrumbMessageCodec(), + new BreadcrumbHtmlRenderer(), + new BreadcrumbOutboundQueue(host.Object) + ); + + router + .BindRowsAsync( + new[] { presentedTarget }, + new[] { new FolderScore(presentedTarget, 730, 0.73) }, + archiveRootPath, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync( + "{\"type\":\"segmentActivate\",\"rowId\":\"row-0\",\"segmentIndex\":1}" + ) + .GetAwaiter() + .GetResult(); + + router.SelectedFolderPath.Should().Be(@"Clients").And.NotBe(hierarchyPath); + } + + [TestMethod] + public void Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget() + { + const string archiveRootPath = @"\\mailbox@example.com\Archive"; + const string presentedTarget = @"Clients\North"; + const string hierarchyPath = @"\\mailbox@example.com\Archive\Clients\North"; + FolderTreeNodeKey leafKey = Key(hierarchyPath); + FolderTreeNodeKey clientsKey = Key(@"\\mailbox@example.com\Archive\Clients"); + var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); + var host = new Mock<IBreadcrumbWebHost>(MockBehavior.Strict); + host.SetupGet(value => value.IsCoreInitialized).Returns(true); + host.Setup(value => value.NavigateToString(It.IsAny<string>())); + host.Setup(value => value.PostMessageJson(It.IsAny<string>())); + provider + .Setup(value => + value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()) + ) + .ReturnsAsync(leafKey); + provider + .Setup(value => value.GetAncestorChainAsync(leafKey, It.IsAny<CancellationToken>())) + .ReturnsAsync(Chain(hierarchyPath, "Clients", "North")); + provider + .Setup(value => + value.GetImmediateSubfoldersAsync(clientsKey, It.IsAny<CancellationToken>()) + ) + .ReturnsAsync(new[] { Segment(hierarchyPath, "North", false) }); + var router = new BreadcrumbBridgeRouter( + provider.Object, + host.Object, + new BreadcrumbMessageCodec(), + new BreadcrumbHtmlRenderer(), + new BreadcrumbOutboundQueue(host.Object) + ); + + router + .BindRowsAsync( + new[] { presentedTarget }, + new[] { new FolderScore(presentedTarget, 730, 0.73) }, + archiveRootPath, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync( + "{\"type\":\"segmentActivate\",\"rowId\":\"row-0\",\"segmentIndex\":1}" + ) + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync("{\"type\":\"leafExpandToggle\",\"rowId\":\"row-0\"}") + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync( + "{\"type\":\"renderedChildActivate\",\"rowId\":\"row-0\",\"childIndex\":0}" + ) + .GetAwaiter() + .GetResult(); + + provider.Verify( + value => value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()), + Times.Once + ); + router.SelectedFolderPath.Should().Be(presentedTarget).And.NotBe(hierarchyPath); + } + + [TestMethod] + public void Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild() + { + // Arrange: pure router seams only; no controls, WebView2, Outlook COM, or message loop. + const string archiveRoot = @"\Archive"; + const string target = @"Clients\North"; + const string fullTarget = @"\Archive\Clients\North"; + FolderTreeNodeKey leafKey = Key(fullTarget); + FolderTreeNodeKey clientsKey = Key(@"\Archive\Clients"); + var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); + var host = new Mock<IBreadcrumbWebHost>(MockBehavior.Strict); + host.SetupGet(h => h.IsCoreInitialized).Returns(true); + host.Setup(h => h.NavigateToString(It.IsAny<string>())); + host.Setup(h => h.PostMessageJson(It.IsAny<string>())); + provider + .Setup(p => p.ResolveLeafKeyAsync(fullTarget, It.IsAny<CancellationToken>())) + .ReturnsAsync(leafKey); + provider + .Setup(p => p.GetAncestorChainAsync(leafKey, It.IsAny<CancellationToken>())) + .ReturnsAsync(Chain(fullTarget, "Clients", "North")); + provider + .Setup(p => + p.GetImmediateSubfoldersAsync(clientsKey, It.IsAny<CancellationToken>()) + ) + .ReturnsAsync( + new[] + { + Segment(@"\Archive\Clients\North", "North", false), + Segment(@"\Archive\Clients\South", "South", false), + } + ); + var router = new BreadcrumbBridgeRouter( + provider.Object, + host.Object, + new BreadcrumbMessageCodec(), + new BreadcrumbHtmlRenderer(), + new BreadcrumbOutboundQueue(host.Object) + ); + + router + .BindRowsAsync( + new[] { target }, + new[] { new FolderScore(target, 730, 0.73) }, + archiveRoot, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + + // Act: make the ancestor active, expand its real immediate children, and select a sibling. + router + .ProcessInboundAsync( + "{\"type\":\"segmentActivate\",\"rowId\":\"row-0\",\"segmentIndex\":1}" + ) + .GetAwaiter() + .GetResult(); + router.SelectedFolderPath.Should().Be(@"Clients"); + router + .ProcessInboundAsync("{\"type\":\"leafExpandToggle\",\"rowId\":\"row-0\"}") + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync( + "{\"type\":\"renderedChildActivate\",\"rowId\":\"row-0\",\"childIndex\":1}" + ) + .GetAwaiter() + .GetResult(); + + // Assert + provider.Verify( + p => p.GetImmediateSubfoldersAsync(clientsKey, It.IsAny<CancellationToken>()), + Times.Once + ); + router.SelectedFolderPath.Should().Be(@"Clients\South"); + } + } +} diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs index 3419dab42..dd0ba8fcf 100644 --- a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs @@ -15,7 +15,7 @@ namespace QuickFiler.Test.Controllers /// Headless Issue #439 regression tests for the pure breadcrumb router seams. /// </summary> [TestClass] - public class BreadcrumbBridgeRouterIssue439Tests + public partial class BreadcrumbBridgeRouterIssue439Tests { [TestMethod] public void Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability() @@ -254,245 +254,6 @@ public void Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProv router.SelectedFolderPath.Should().Be(canceled); } - [TestMethod] - public void Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget() - { - const string archiveRootPath = @"\\mailbox@example.com\Archive"; - const string presentedTarget = @"Clients\North"; - const string hierarchyPath = @"\\mailbox@example.com\Archive\Clients\North"; - FolderTreeNodeKey key = Key(hierarchyPath); - var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); - var host = new Mock<IBreadcrumbWebHost>(MockBehavior.Strict); - host.SetupGet(value => value.IsCoreInitialized).Returns(true); - host.Setup(value => value.NavigateToString(It.IsAny<string>())); - host.Setup(value => value.PostMessageJson(It.IsAny<string>())); - provider - .Setup(value => - value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()) - ) - .ReturnsAsync(key); - provider - .Setup(value => value.GetAncestorChainAsync(key, It.IsAny<CancellationToken>())) - .ReturnsAsync(Chain(hierarchyPath, "Clients", "North")); - var router = new BreadcrumbBridgeRouter( - provider.Object, - host.Object, - new BreadcrumbMessageCodec(), - new BreadcrumbHtmlRenderer(), - new BreadcrumbOutboundQueue(host.Object) - ); - - router - .BindRowsAsync( - new[] { presentedTarget }, - new[] { new FolderScore(presentedTarget, 730, 0.73) }, - archiveRootPath, - CancellationToken.None - ) - .GetAwaiter() - .GetResult(); - router - .ProcessInboundAsync("{\"type\":\"rowSelected\",\"rowId\":\"row-0\"}") - .GetAwaiter() - .GetResult(); - - provider.Verify( - value => value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()), - Times.Once - ); - router.SelectedFolderPath.Should().Be(presentedTarget).And.NotBe(hierarchyPath); - } - - [TestMethod] - public void Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget() - { - const string archiveRootPath = @"\\mailbox@example.com\Archive"; - const string presentedTarget = @"Clients\North"; - const string hierarchyPath = @"\\mailbox@example.com\Archive\Clients\North"; - FolderTreeNodeKey leafKey = Key(hierarchyPath); - var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); - var host = new Mock<IBreadcrumbWebHost>(MockBehavior.Strict); - host.SetupGet(value => value.IsCoreInitialized).Returns(true); - host.Setup(value => value.NavigateToString(It.IsAny<string>())); - host.Setup(value => value.PostMessageJson(It.IsAny<string>())); - provider - .Setup(value => - value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()) - ) - .ReturnsAsync(leafKey); - provider - .Setup(value => value.GetAncestorChainAsync(leafKey, It.IsAny<CancellationToken>())) - .ReturnsAsync(Chain(hierarchyPath, "Clients", "North")); - var router = new BreadcrumbBridgeRouter( - provider.Object, - host.Object, - new BreadcrumbMessageCodec(), - new BreadcrumbHtmlRenderer(), - new BreadcrumbOutboundQueue(host.Object) - ); - - router - .BindRowsAsync( - new[] { presentedTarget }, - new[] { new FolderScore(presentedTarget, 730, 0.73) }, - archiveRootPath, - CancellationToken.None - ) - .GetAwaiter() - .GetResult(); - router - .ProcessInboundAsync( - "{\"type\":\"segmentActivate\",\"rowId\":\"row-0\",\"segmentIndex\":1}" - ) - .GetAwaiter() - .GetResult(); - - router.SelectedFolderPath.Should().Be(@"Clients").And.NotBe(hierarchyPath); - } - - [TestMethod] - public void Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget() - { - const string archiveRootPath = @"\\mailbox@example.com\Archive"; - const string presentedTarget = @"Clients\North"; - const string hierarchyPath = @"\\mailbox@example.com\Archive\Clients\North"; - FolderTreeNodeKey leafKey = Key(hierarchyPath); - FolderTreeNodeKey clientsKey = Key(@"\\mailbox@example.com\Archive\Clients"); - var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); - var host = new Mock<IBreadcrumbWebHost>(MockBehavior.Strict); - host.SetupGet(value => value.IsCoreInitialized).Returns(true); - host.Setup(value => value.NavigateToString(It.IsAny<string>())); - host.Setup(value => value.PostMessageJson(It.IsAny<string>())); - provider - .Setup(value => - value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()) - ) - .ReturnsAsync(leafKey); - provider - .Setup(value => value.GetAncestorChainAsync(leafKey, It.IsAny<CancellationToken>())) - .ReturnsAsync(Chain(hierarchyPath, "Clients", "North")); - provider - .Setup(value => - value.GetImmediateSubfoldersAsync(clientsKey, It.IsAny<CancellationToken>()) - ) - .ReturnsAsync(new[] { Segment(hierarchyPath, "North", false) }); - var router = new BreadcrumbBridgeRouter( - provider.Object, - host.Object, - new BreadcrumbMessageCodec(), - new BreadcrumbHtmlRenderer(), - new BreadcrumbOutboundQueue(host.Object) - ); - - router - .BindRowsAsync( - new[] { presentedTarget }, - new[] { new FolderScore(presentedTarget, 730, 0.73) }, - archiveRootPath, - CancellationToken.None - ) - .GetAwaiter() - .GetResult(); - router - .ProcessInboundAsync( - "{\"type\":\"segmentActivate\",\"rowId\":\"row-0\",\"segmentIndex\":1}" - ) - .GetAwaiter() - .GetResult(); - router - .ProcessInboundAsync("{\"type\":\"leafExpandToggle\",\"rowId\":\"row-0\"}") - .GetAwaiter() - .GetResult(); - router - .ProcessInboundAsync( - "{\"type\":\"renderedChildActivate\",\"rowId\":\"row-0\",\"childIndex\":0}" - ) - .GetAwaiter() - .GetResult(); - - provider.Verify( - value => value.ResolveLeafKeyAsync(hierarchyPath, It.IsAny<CancellationToken>()), - Times.Once - ); - router.SelectedFolderPath.Should().Be(presentedTarget).And.NotBe(hierarchyPath); - } - - [TestMethod] - public void Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild() - { - // Arrange: pure router seams only; no controls, WebView2, Outlook COM, or message loop. - const string archiveRoot = @"\Archive"; - const string target = @"Clients\North"; - const string fullTarget = @"\Archive\Clients\North"; - FolderTreeNodeKey leafKey = Key(fullTarget); - FolderTreeNodeKey clientsKey = Key(@"\Archive\Clients"); - var provider = new Mock<IFolderHierarchyProvider>(MockBehavior.Strict); - var host = new Mock<IBreadcrumbWebHost>(MockBehavior.Strict); - host.SetupGet(h => h.IsCoreInitialized).Returns(true); - host.Setup(h => h.NavigateToString(It.IsAny<string>())); - host.Setup(h => h.PostMessageJson(It.IsAny<string>())); - provider - .Setup(p => p.ResolveLeafKeyAsync(fullTarget, It.IsAny<CancellationToken>())) - .ReturnsAsync(leafKey); - provider - .Setup(p => p.GetAncestorChainAsync(leafKey, It.IsAny<CancellationToken>())) - .ReturnsAsync(Chain(fullTarget, "Clients", "North")); - provider - .Setup(p => - p.GetImmediateSubfoldersAsync(clientsKey, It.IsAny<CancellationToken>()) - ) - .ReturnsAsync( - new[] - { - Segment(@"\Archive\Clients\North", "North", false), - Segment(@"\Archive\Clients\South", "South", false), - } - ); - var router = new BreadcrumbBridgeRouter( - provider.Object, - host.Object, - new BreadcrumbMessageCodec(), - new BreadcrumbHtmlRenderer(), - new BreadcrumbOutboundQueue(host.Object) - ); - - router - .BindRowsAsync( - new[] { target }, - new[] { new FolderScore(target, 730, 0.73) }, - archiveRoot, - CancellationToken.None - ) - .GetAwaiter() - .GetResult(); - - // Act: make the ancestor active, expand its real immediate children, and select a sibling. - router - .ProcessInboundAsync( - "{\"type\":\"segmentActivate\",\"rowId\":\"row-0\",\"segmentIndex\":1}" - ) - .GetAwaiter() - .GetResult(); - router.SelectedFolderPath.Should().Be(@"Clients"); - router - .ProcessInboundAsync("{\"type\":\"leafExpandToggle\",\"rowId\":\"row-0\"}") - .GetAwaiter() - .GetResult(); - router - .ProcessInboundAsync( - "{\"type\":\"renderedChildActivate\",\"rowId\":\"row-0\",\"childIndex\":1}" - ) - .GetAwaiter() - .GetResult(); - - // Assert - provider.Verify( - p => p.GetImmediateSubfoldersAsync(clientsKey, It.IsAny<CancellationToken>()), - Times.Once - ); - router.SelectedFolderPath.Should().Be(@"Clients\South"); - } - [TestMethod] public void Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows() { diff --git a/QuickFiler.Test/QuickFiler.Test.csproj b/QuickFiler.Test/QuickFiler.Test.csproj index 2555dfe75..c7395746f 100644 --- a/QuickFiler.Test/QuickFiler.Test.csproj +++ b/QuickFiler.Test/QuickFiler.Test.csproj @@ -62,6 +62,7 @@ <Compile Include="Controllers\BreadcrumbBridgeRouterIssue614Tests.cs" /> <Compile Include="Controllers\EfcSelectionGuardTests.cs" /> <Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.cs" /> + <Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.Activation.cs" /> <Compile Include="Controllers\BreadcrumbBridgeRouterIssue637Tests.cs" /> <Compile Include="Viewers\BreadcrumbBridgeCoordinatorTests.cs" /> <Compile Include="Viewers\BreadcrumbBridgeCoordinatorSupersessionTests.cs" /> @@ -508,4 +509,4 @@ <Analyzer Include="..\packages\Microsoft.CodeAnalysis.BannedApiAnalyzers.5.6.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.CSharp.BannedApiAnalyzers.dll" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)..\BannedSymbols.txt" /> </ItemGroup> -</Project> \ No newline at end of file +</Project> diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-32.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-32.md new file mode 100644 index 000000000..04f9617cd --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-32.md @@ -0,0 +1,29 @@ +# Code Review: Issue #637 rooted breadcrumb selection normalization + +## Executive Summary + +The implementation correctly confines normalization to full Outlook paths with a bound archive root, preserves archive-root-exact and no-bound-root behavior, and routes filing normalization through a total pure helper. Current-head formatter, analyzer, nullable, and full MSTest coverage checks passed. One blocking maintainability finding remains: a modified test fixture is 694 lines, exceeding the repository policy limit. + +## Findings Table + +| Severity | File | Location | Finding | Recommendation | Rationale | Evidence | +|---|---|---|---|---|---|---| +| Blocker | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` | `:1-694` | The feature modifies a 694-line test file, exceeding the 500-line limit for modified test code. | Split the Issue #439 fixture into cohesive test classes/files below 500 lines; preserve test names/semantics and add required non-SDK project compile includes. Rerun the full C# toolchain. | The repository policy applies the limit to modified test code and provides no legacy-file exception. | Line count is 694; `git diff main...HEAD` shows the file is modified; `AGENTS.md` policy. | + +## Reviewed Implementation + +- `BreadcrumbBridgeRouter.Selection.SelectRow` rejects out-of-root and root-exact full paths without calling `CommitSelection`, and commits the relative stem for eligible rooted targets. +- `EfcDataModel.ToFilingStemOrVerbatim` returns a non-empty archive-relative stem only for a rooted candidate under its supplied ancestor; all other values are preserved verbatim. +- `MoveToFolderAsync(string, ...)` delegates only its `DestinationOlStem` value to the helper; unrelated overloads and folder-opening paths are not changed. +- The corrected Issue #439 assertion retains the rooted provider lookup while asserting the new relative selected value. AC21 records this as a deliberate specification correction, not a weakened test. + +## Verification Evidence + +- `git diff --check main...HEAD` passed. +- CSharpier format and check passed at `a314228b9c3d9a4944a9e88e1a4eb4bd9c4b0f7b`. +- Analyzer and nullable rebuilds passed with 0 errors; each retained five existing `System.Reactive` packages.config warnings. +- Current-head coverage passed 6,894 tests with 0 failures across 9 assemblies and reported 85.3545% line coverage. + +## Conclusion + +The behavioral implementation has no additional code-level blocker. PR readiness is blocked by the test-file-size policy finding until it is remediated. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p1-t1-issue439-split-map.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p1-t1-issue439-split-map.md new file mode 100644 index 000000000..a507e57d2 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p1-t1-issue439-split-map.md @@ -0,0 +1,21 @@ +# P1-T1 Issue #439 partial-fixture split map + +Timestamp: 2026-08-31T17-09 + +The following four contiguous activation tests move, in their existing source order, to `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs`: + +1. `Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget` +2. `Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget` +3. `Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget` +4. `Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild` + +The original `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` retains these six tests: + +1. `Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` +2. `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` +3. `Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome` +4. `Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows` +5. `Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic` +6. `Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection` + +It also retains the `Key`, `Chain`, and `Segment` private helpers. Both files declare the same partial MSTest class, `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests`; therefore every test retains its existing fully qualified method identity. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t1-remediation-scope-audit.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t1-remediation-scope-audit.md new file mode 100644 index 000000000..d43e6d978 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t1-remediation-scope-audit.md @@ -0,0 +1,35 @@ +# P3-T1 remediation scope audit + +Timestamp: 2026-08-31T17-17 + +Command: `git diff --name-only; git status --short; git diff --quiet -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md; git diff --check` + +EXIT_CODE: 0 + +Output Summary: The remediation implementation diff is limited to the approved two partial test fixtures and their single project compile include. No production file or `spec.md` change is present. The separately listed plan and evidence paths are documentation only. The modified orchestration checkpoint is orchestrator-owned and outside this executor’s implementation scope. + +Implementation paths changed by this remediation: + +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` +- `QuickFiler.Test/QuickFiler.Test.csproj` + +Remediation plan and evidence updates: + +- `remediation-plan.2026-08-31T13-33.md` +- `evidence/remediation-baseline/p0-t5-mstest-coverage-retry.md` +- `evidence/remediation-baseline/p0-t5-mstest-coverage-retry.stdout.log` +- `evidence/remediation-baseline/p0-t5-mstest-coverage-retry.stderr.log` +- `evidence/remediation-baseline/p0-t6-issue439-fixture-inventory.md` +- `evidence/other/p1-t1-issue439-split-map.md` +- `evidence/qa-gates/p1-t5-fixture-split-verification.md` +- `evidence/qa-gates/p2-t1-csharpier-format.md` +- `evidence/qa-gates/p2-t2-csharpier-check.md` +- `evidence/qa-gates/p2-t3-msbuild-analyzers.md` +- `evidence/qa-gates/p2-t4-msbuild-nullable.md` +- `evidence/qa-gates/p2-t5-mstest-coverage.md` +- `evidence/qa-gates/p2-t6-coverage-comparison.md` +- `evidence/qa-gates/p2-t7-remediation-qa-audit.md` +- `evidence/other/p3-t1-remediation-scope-audit.md` + +`git diff --quiet` for `spec.md` exited 0. No production-code path appears in the remediation implementation list, and `git diff --check` reported no whitespace error. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t2-remediation-commit-handoff.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t2-remediation-commit-handoff.md new file mode 100644 index 000000000..109f2d19f --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t2-remediation-commit-handoff.md @@ -0,0 +1,47 @@ +# P3-T2 remediation commit handoff + +Timestamp: 2026-08-31T17-18 + +The required verification artifacts are complete: + +- P1-T5: `evidence/qa-gates/p1-t5-fixture-split-verification.md` +- P2-T7: `evidence/qa-gates/p2-t7-remediation-qa-audit.md` +- P3-T1: `evidence/other/p3-t1-remediation-scope-audit.md` + +The orchestrator must stage exactly these remediation source and project files: + +1. `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` +2. `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` +3. `QuickFiler.Test/QuickFiler.Test.csproj` + +The orchestrator must also stage these review and remediation records: + +4. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-32.md` +5. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-32.md` +6. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-32.md` +7. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-inputs.2026-08-31T13-32.md` +8. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-plan.2026-08-31T13-33.md` +9. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t1-policy-read.md` +10. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t2-csharpier-check.md` +11. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t3-msbuild-analyzers.md` +12. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t4-msbuild-nullable.md` +13. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage.md` +14. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage-retry.md` +15. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage-retry.stdout.log` +16. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage-retry.stderr.log` +17. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t6-issue439-fixture-inventory.md` +18. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p1-t1-issue439-split-map.md` +19. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p1-t5-fixture-split-verification.md` +20. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t1-csharpier-format.md` +21. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t2-csharpier-check.md` +22. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t3-msbuild-analyzers.md` +23. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t4-msbuild-nullable.md` +24. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t5-mstest-coverage.md` +25. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t6-coverage-comparison.md` +26. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t7-remediation-qa-audit.md` +27. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t1-remediation-scope-audit.md` +28. `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t2-remediation-commit-handoff.md` + +Do not stage `artifacts/orchestration/orchestrator-state.json`; it is orchestrator-owned. Do not stage generated `coverage/*.cobertura.xml` files. + +PROGRESS_COMMIT_REQUIRED: remediation-fixture-split diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p1-t5-fixture-split-verification.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p1-t5-fixture-split-verification.md new file mode 100644 index 000000000..38353b103 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p1-t5-fixture-split-verification.md @@ -0,0 +1,34 @@ +# P1-T5 Issue #439 fixture split verification + +Timestamp: 2026-08-31T17-10 + +Command: `Get-Content fixture line counts; rg public test methods and Compile Include entries; git diff --exit-code -- docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md; parse QuickFiler.Test/QuickFiler.Test.csproj as XML` + +EXIT_CODE: 0 + +Output Summary: The original partial fixture is 455 lines and the activation partial fixture is 253 lines. Each is within the 500-line limit. The two partial files provide exactly the ten pre-split test identities, with one explicit compile include each. `spec.md` is byte-for-byte unchanged from `HEAD`, including AC21. + +Fixture line counts: + +- `BreadcrumbBridgeRouterIssue439Tests.cs`: 455 +- `BreadcrumbBridgeRouterIssue439Tests.Activation.cs`: 253 + +Fully qualified test identities (each appears exactly once): + +1. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` +2. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` +3. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome` +4. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget` +5. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget` +6. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget` +7. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild` +8. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows` +9. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic` +10. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection` + +Explicit project items: + +- `<Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.cs" />` appears once. +- `<Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.Activation.cs" />` appears once. + +The project file parsed as well-formed XML. `git diff --exit-code` for `spec.md` returned 0; AC21 remains byte-for-byte unchanged. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t1-csharpier-format.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t1-csharpier-format.md new file mode 100644 index 000000000..734be490a --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t1-csharpier-format.md @@ -0,0 +1,9 @@ +# P2-T1 CSharpier format + +Timestamp: 2026-08-31T17-11 + +Command: `dotnet tool run csharpier format .` + +EXIT_CODE: 0 + +Output Summary: CSharpier formatted 1,565 files and exited successfully in 2,376 milliseconds. The remediation split source remains limited to the planned partial-fixture changes. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t2-csharpier-check.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t2-csharpier-check.md new file mode 100644 index 000000000..45f06593b --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t2-csharpier-check.md @@ -0,0 +1,9 @@ +# P2-T2 CSharpier check + +Timestamp: 2026-08-31T17-11 + +Command: `dotnet tool run csharpier check .` + +EXIT_CODE: 0 + +Output Summary: CSharpier checked 1,565 files in 4,968 milliseconds and reported no formatting violations. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t3-msbuild-analyzers.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t3-msbuild-analyzers.md new file mode 100644 index 000000000..f625c8a83 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t3-msbuild-analyzers.md @@ -0,0 +1,9 @@ +# P2-T3 analyzer rebuild + +Timestamp: 2026-08-31T17-13 + +Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + +EXIT_CODE: 0 + +Output Summary: The analyzer-enabled rebuild succeeded in 15.71 seconds with 0 errors. It reported five existing `System.Reactive` packages.config compatibility warnings across unrelated projects; no analyzer error was reported for the remediation fixture split. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t4-msbuild-nullable.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t4-msbuild-nullable.md new file mode 100644 index 000000000..239568ce1 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t4-msbuild-nullable.md @@ -0,0 +1,9 @@ +# P2-T4 nullable rebuild + +Timestamp: 2026-08-31T17-14 + +Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + +EXIT_CODE: 0 + +Output Summary: The nullable and warnings-as-errors rebuild succeeded in 12.70 seconds with 0 errors. The output included five pre-existing `System.Reactive` packages.config compatibility warnings from unrelated projects; the remediation fixture split introduced no compiler or nullable error. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t5-mstest-coverage.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t5-mstest-coverage.md new file mode 100644 index 000000000..eb9e266fd --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t5-mstest-coverage.md @@ -0,0 +1,11 @@ +# P2-T5 post-split MSTest coverage + +Timestamp: 2026-08-31T17-15 + +Command: `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-remediation.cobertura.xml` + +EXIT_CODE: 0 + +Output Summary: The wrapper discovered nine test assemblies and completed successfully. MSTest reported 6,894 total tests, all passed, with 0 failures. The generated Cobertura report records 85.3389% repository line coverage. + +Generated Cobertura path: `coverage/p7-t5-remediation.cobertura.xml`. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t6-coverage-comparison.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t6-coverage-comparison.md new file mode 100644 index 000000000..6ecd750d3 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t6-coverage-comparison.md @@ -0,0 +1,23 @@ +# P2-T6 coverage comparison + +Timestamp: 2026-08-31T17-16 + +Command: `Parse coverage/remediation-baseline.cobertura.xml and coverage/p7-t5-remediation.cobertura.xml line-rate values; inspect remediation diff paths` + +EXIT_CODE: 0 + +Output Summary: Post-split repository line coverage is 85.3389%, which exceeds the 80% floor and is 0.0031 percentage points above the 85.3358% baseline. The remediation changes only test and project surfaces, so no changed production-code sequence points exist to calculate. + +- Baseline repository line coverage: 85.3358% +- Post-change repository line coverage: 85.3389% +- Coverage delta: +0.0031 percentage points +- Repository coverage floor: 80.0000% — passed +- No-regression check: passed + +Changed implementation paths: + +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` +- `QuickFiler.Test/QuickFiler.Test.csproj` + +No production-code path is changed; therefore changed production-code sequence points are not applicable. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t7-remediation-qa-audit.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t7-remediation-qa-audit.md new file mode 100644 index 000000000..74ae7e421 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t7-remediation-qa-audit.md @@ -0,0 +1,15 @@ +# P2-T7 remediation QA audit + +Timestamp: 2026-08-31T17-16 + +The final C# QA pass completed in the required order with no skipped planned command: + +1. P2-T1 — [p2-t1-csharpier-format.md](p2-t1-csharpier-format.md): `EXIT_CODE: 0`. +2. P2-T2 — [p2-t2-csharpier-check.md](p2-t2-csharpier-check.md): `EXIT_CODE: 0`. +3. P2-T3 — [p2-t3-msbuild-analyzers.md](p2-t3-msbuild-analyzers.md): `EXIT_CODE: 0`. +4. P2-T4 — [p2-t4-msbuild-nullable.md](p2-t4-msbuild-nullable.md): `EXIT_CODE: 0`. +5. P2-T5 — [p2-t5-mstest-coverage.md](p2-t5-mstest-coverage.md): `EXIT_CODE: 0`, 6,894 passing tests, and 85.3389% repository line coverage. + +The fixture-size evidence in [p1-t5-fixture-split-verification.md](p1-t5-fixture-split-verification.md) records 455 lines for the retained fixture and 253 lines for the activation fixture; both satisfy the 500-line limit. + +All final-pass exit codes are zero. No planned command was skipped. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t1-policy-read.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t1-policy-read.md new file mode 100644 index 000000000..65a30cffb --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t1-policy-read.md @@ -0,0 +1,24 @@ +# P0-T1 Policy Read + +Timestamp: 2026-08-31T13:34:00Z + +Policy Order: + +1. `AGENTS.md` standing instructions +2. `AGENTS.md` general code-change instructions +3. `AGENTS.md` general unit-test instructions +4. `.agents/skills/csharp/SKILL.md` + +Exact files read: + +- `AGENTS.md` +- `.agents/skills/policy-compliance-order/SKILL.md` +- `.agents/skills/acceptance-criteria-tracking/SKILL.md` +- `.agents/skills/csharp/SKILL.md` + +Relevant requirements: + +- Production, test, and reusable script files must not exceed 500 lines. +- C# unit tests use MSTest, Moq, and FluentAssertions. +- Unit tests must not create or use temporary files. +- The C# toolchain runs CSharpier, analyzers, nullable analysis, then MSTest coverage, restarting after a failure or formatting change. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t2-csharpier-check.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t2-csharpier-check.md new file mode 100644 index 000000000..8b129678d --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t2-csharpier-check.md @@ -0,0 +1,9 @@ +# P0-T2 CSharpier Check + +Timestamp: 2026-08-31T13:34:30Z + +Command: `dotnet tool run csharpier check .` + +EXIT_CODE: 0 + +Output Summary: `Checked 1564 files in 4603ms.` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t3-msbuild-analyzers.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t3-msbuild-analyzers.md new file mode 100644 index 000000000..e7c883114 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t3-msbuild-analyzers.md @@ -0,0 +1,9 @@ +# P0-T3 Analyzer Build + +Timestamp: 2026-08-31T13:35:00Z + +Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + +EXIT_CODE: 0 + +Output Summary: Build succeeded with 0 errors and 5 existing `System.Reactive` packages.config compatibility warnings. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t4-msbuild-nullable.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t4-msbuild-nullable.md new file mode 100644 index 000000000..891b08bb4 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t4-msbuild-nullable.md @@ -0,0 +1,9 @@ +# P0-T4 Nullable Build + +Timestamp: 2026-08-31T13:35:30Z + +Command: `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + +EXIT_CODE: 0 + +Output Summary: Build succeeded with 0 errors and 5 existing `System.Reactive` packages.config compatibility warnings. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage-retry.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage-retry.md new file mode 100644 index 000000000..ca2e5d085 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage-retry.md @@ -0,0 +1,13 @@ +# P0-T5 bounded MSTest coverage baseline retry + +Timestamp: 2026-08-31T17-07 + +Command: `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\remediation-baseline.cobertura.xml` + +EXIT_CODE: 0 + +Output Summary: The unchanged wrapper command completed within the external 15-minute ceiling in 60.682 seconds. It discovered nine test assemblies and reported 6,894 total tests, all passed, with 0 failures. The generated Cobertura report records 85.3358% repository line coverage. + +Cobertura output status: generated at `coverage/remediation-baseline.cobertura.xml`. + +Observed retry process: wrapper `pwsh` PID 41616. No descendant process remained after the wrapper exited. No owned process termination was required. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage.md new file mode 100644 index 000000000..e2d414e5a --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage.md @@ -0,0 +1,11 @@ +# P0-T5 MSTest Coverage Baseline + +Timestamp: 2026-08-31T13:35:45Z + +Command: `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\remediation-baseline.cobertura.xml` + +EXIT_CODE: unavailable; the owned coverage process tree was stopped after sustained non-progress. + +Output Summary: The wrapper discovered nine test assemblies and began the run under `dotnet-coverage`. It did not emit `coverage/remediation-baseline.cobertura.xml`; `dotnet-coverage` PID 84620, `vstest.console` PID 88472, and its child testhost remained active without further CPU progress. Because no Cobertura report was generated, numeric line coverage and final passing/failing totals are unavailable. This task remains unchecked pending a bounded coverage-run recovery. + +Generated Cobertura path: `coverage/remediation-baseline.cobertura.xml` (not generated) diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t6-issue439-fixture-inventory.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t6-issue439-fixture-inventory.md new file mode 100644 index 000000000..8afcbbaea --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t6-issue439-fixture-inventory.md @@ -0,0 +1,32 @@ +# P0-T6 Issue #439 fixture inventory + +Timestamp: 2026-08-31T17-08 + +Command: `Get-Content QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs; rg test methods and helpers; rg BreadcrumbBridgeRouterIssue439Tests.cs QuickFiler.Test/QuickFiler.Test.csproj` + +EXIT_CODE: 0 + +Output Summary: The pre-split fixture has 694 lines and ten MSTest methods. Its only explicit project compile item is `Controllers\BreadcrumbBridgeRouterIssue439Tests.cs`. + +Line count: 694 + +Fully qualified test method identities: + +1. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` +2. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` +3. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome` +4. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget` +5. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget` +6. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget` +7. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild` +8. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows` +9. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic` +10. `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection` + +Private helper signatures retained in the original fixture: + +- `private static FolderTreeNodeKey Key(string path)` +- `private static IReadOnlyList<FolderBreadcrumbSegment> Chain(string leafPath, string middleName, string leafName)` +- `private static FolderBreadcrumbSegment Segment(string path, string name, bool hasChildren)` + +Current explicit compile include: `<Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.cs" />`. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-32.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-32.md new file mode 100644 index 000000000..e55224ff6 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-32.md @@ -0,0 +1,40 @@ +# Feature Audit: Issue #637 rooted breadcrumb selection normalization + +## Scope and Baseline + +This review compares `a314228b9c3d9a4944a9e88e1a4eb4bd9c4b0f7b` to `main` at merge base `3be3f237a8551df3f27f83d9d1af2f26074fc93a`. The canonical PR-context artifacts were collected against `main`. The active feature folder is `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. + +The full feature-vs-base diff comprises 95 changed files: 8 C#, 2 project files, and 85 Markdown files. Current-head C# QA passed: formatter and check, analyzer rebuild, nullable rebuild, and 6,894 passing MSTest tests with 85.3545% line coverage. + +## Acceptance Criteria Inventory + +The persisted work mode is `full-bug`; `spec.md` is the authoritative source. It contains 30 acceptance-criteria checkboxes, all currently marked delivered. AC21's deliberate specification-correction wording is retained unchanged. + +## Acceptance Criteria Evaluation + +| Criterion | Status | Evidence | +|---|---|---| +| AC1-AC5 | PASS | Named Issue #637 tests cover root-exact, rooted descendant, case/trailing separator, out-of-root, and separator-boundary behavior. | +| AC6-AC10 | PASS | Relative/trash pass-through, untouched hierarchy/commit paths, family shape, and `SelectFirstRow` reachability are covered by source review and P8 evidence. | +| AC11-AC14 | PASS | The new partial-file helper is pure, gated, normalized when eligible, and total/no-throw under documented cases. | +| AC15-AC17 | PASS | Existing archive-stem behavior, MoveToFolder family shape, and folder-opening/degradation paths are preserved. | +| AC18-AC21 | PASS | The Issue #439 assertion and wording were corrected while provider lookup remains rooted; AC21 remains explicitly an invariant-driven specification correction. | +| AC22-AC24 | PASS | Stale deferrals were corrected, guard behavior remains covered, and the SelectedFolderPath surface is preserved. | +| AC25 | PASS | The specification's stated no-growth disposition for the existing 694-line fixture is met; the separate policy audit identifies that repository policy supplies no exception for modifying it. | +| AC26-AC30 | PASS | Compile include, nullable, ordered toolchain, canonical evidence/coverage, and scope-boundary evidence pass. | + +## Summary + +All 30 product acceptance criteria are substantively met. The feature is not PR-ready because the policy audit has a blocker independent of product behavior: the modified Issue #439 test file is 694 lines, above the repository maximum of 500 for test code. + +## Acceptance Criteria Check-off + +No acceptance checkbox was changed by this review. Each was already checked and is evaluated PASS above. The policy blocker is separately recorded because AC25 documents a pre-existing-size/no-growth disposition, whereas repository policy provides no exception for modification of the oversized file. + +### Acceptance Criteria Status + +- Source: `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md` +- Total AC items: 30 +- Checked off (delivered): 30 +- Remaining (unchecked): 0 +- Items remaining: none diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-32.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-32.md new file mode 100644 index 000000000..490cbc21e --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-32.md @@ -0,0 +1,108 @@ +# Policy Compliance Audit: Issue #637 rooted breadcrumb selection normalization + +**Audit Date:** 2026-08-31 +**Base / Head:** `main` (`3be3f237a8551df3f27f83d9d1af2f26074fc93a`) / `a314228b9c3d9a4944a9e88e1a4eb4bd9c4b0f7b` + +| Language | Files changed | Tests | Result | Baseline line coverage | Post-change line coverage | Changed/new code coverage | +|---|---:|---:|---|---:|---:|---| +| C# | 8 | 6,894 | PASS, 6,894 passed and 0 failed | 85.3428% | 85.3545% | 100.0% — changed production lines are covered | +| MSBuild XML | 2 | N/A | PASS; analyzer and nullable rebuilds passed | N/A | N/A | N/A | +| Markdown | 85 | N/A | PASS; `git diff --check main...HEAD` passed | N/A | N/A | N/A | + +### Coverage Evidence Checklist + +- TypeScript baseline coverage artifact: `N/A - out of scope; zero TypeScript files changed` +- TypeScript post-change coverage artifact: `N/A - out of scope; zero TypeScript files changed` +- PowerShell baseline coverage artifact: `N/A - out of scope; zero PowerShell files changed` +- PowerShell post-change coverage artifact: `N/A - out of scope; zero PowerShell files changed` +- Per-language comparison summary: `### 1.2.1 Per-Language Coverage Comparison` + +### 1.2.1 Per-Language Coverage Comparison + +- C#: Baseline: 85.3428% lines -> Post-change: 85.3545% lines. Change: +0.0117 percentage points. New/changed-code coverage: 100.0% of emitted sequence points for `ToFilingStemOrVerbatim`, with no zero-hit changed-line intersection. Disposition: PASS. Evidence: `evidence/baseline/p0-t16-coverage-headline.md`, `coverage/p7-t5-postmerge.cobertura.xml`, and `evidence/qa-gates/p7-t7-changed-line-coverage.md`. + +## Executive Summary + +The functional change and current-head C# toolchain evidence support rooted-path normalization. The audit is not compliant overall because the feature modifies `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, which has 694 lines. The repository policy applies the 500-line limit to modified test files, with no applicable exception. The specification's no-growth record does not override that policy. + +## 1. General Unit Test Policy Compliance + +| Requirement | Status | Evidence | +|---|---|---| +| Independence and isolation | PASS | New router tests use Moq seams and clean up their in-memory log appender; helper tests invoke a pure static method. | +| Determinism and test results | PASS | Current-head coverage run: 9 assemblies, 6,894 passed, 0 failed. | +| Positive, negative, and boundary scenarios | PASS | Tests cover under-root, root-exact, out-of-root, boundary-near-miss, no-bound-root, relative, trash, and `SelectFirstRow` routes. | +| External dependencies and temporary files | PASS | Changed tests use mocks and no temporary-file APIs. | +| Readability and maintainability | FAIL | A modified test fixture remains 694 lines. | + +## 2. General Code Change Policy Compliance + +| Requirement | Status | Evidence | +|---|---|---| +| Objective and plan | PASS | `issue.md`, `spec.md`, research, and the atomic plan document the change. | +| Simplicity and separation of concerns | PASS | `SelectRow` normalizes only full rooted paths; a pure partial-file helper preserves the data-model size boundary. | +| Naming, contracts, and comments | PASS | The helper's documented total no-throw contract and stale deferral correction match its behavior. | +| Cohesive modules and 500-line maximum | FAIL | `BreadcrumbBridgeRouterIssue439Tests.cs` is modified and 694 lines. | +| Toolchain execution | PASS | Current-head CSharpier, analyzer, nullable, and MSTest coverage commands passed. | + +## 3. Language-Specific Code Change Policy Compliance + +### C# + +| Requirement | Status | Evidence | +|---|---|---| +| CSharpier formatting | PASS | Current-head format and check passed. | +| Analyzer diagnostics | PASS | Analyzer rebuild passed with 0 errors; five existing `System.Reactive` packages.config warnings remained. | +| Nullable posture | PASS | Warnings-as-errors rebuild passed with 0 errors. | +| Focused design | PASS | The new partial type avoids extending `EfcDataModel.cs` beyond 500 lines and adds no public API. | + +## 4. Language-Specific Unit Test Policy Compliance + +| Requirement | Status | Evidence | +|---|---|---| +| MSTest framework | PASS | Changed tests use `[TestClass]` and `[TestMethod]`. | +| Moq and FluentAssertions | PASS | Router tests use Moq and FluentAssertions; pure tests do not add unnecessary mocks. | +| File-size limit for modified tests | FAIL | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` is 694 lines, above the policy maximum. | + +## 5. Test Coverage Detail + +Baseline line coverage was 85.3428% (`evidence/baseline/p0-t16-coverage-headline.md`). Current-head Cobertura reports 54,836 covered of 64,245 valid lines, or 85.3545%. Existing changed-line evidence records no zero-hit intersection and 100% condition coverage (6/6) for the helper gate. Coverage meets the 80% repository floor and the new-helper target. + +## 6. Test Execution Metrics + +The post-merge coverage run discovered 9 assemblies and reported 6,894 passed, 0 failed. No current-head test failure was observed. + +## 7. Code Quality Checks + +`git diff --check main...HEAD` passed. CSharpier, analyzer rebuild, nullable rebuild, and MSTest coverage passed. The remaining quality failure is structural: the modified Issue #439 test fixture exceeds the maximum file length. + +## 8. Gaps and Exceptions + +The specification records that the 694-line Issue #439 fixture did not grow, but that is not an approved repository-policy exception. Remediation must split or reduce it below 500 lines while preserving test semantics and non-SDK project compile includes. + +## 9. Summary of Changes + +The feature normalizes rooted Outlook paths in `SelectRow`, adds a total filing-boundary helper, corrects one Issue #439 expected selection as the deliberate AC21 specification correction, and updates stale deferral records. + +## 10. Compliance Verdict + +FAIL — remediation is required before PR readiness because a modified test file exceeds the repository 500-line limit. The other reviewed behavioral, toolchain, and coverage checks passed. + +## Appendix A: Test Inventory + +- `BreadcrumbBridgeRouterIssue637Tests`: rooted-path selection coverage. +- `EfcDataModelIssue614Tests`: archive-stem and helper coverage. +- `BreadcrumbBridgeRouterIssue439Tests`: corrected expected selection and retained provider-lookup assertion. +- `EfcSelectionGuardTests`: unchanged behavior with stale-deferral wording correction. + +## Appendix B: Toolchain Commands Reference + +`dotnet tool run csharpier format .` + +`dotnet tool run csharpier check .` + +`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + +`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + +`pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-postmerge.cobertura.xml` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-inputs.2026-08-31T13-32.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-inputs.2026-08-31T13-32.md new file mode 100644 index 000000000..c9f75ebc2 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-inputs.2026-08-31T13-32.md @@ -0,0 +1,41 @@ +# Remediation Inputs: Issue #637 post-merge feature review + +Timestamp: 2026-08-31T13-32 + +## Authoritative Finding + +The post-merge policy audit is FAIL because `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` is modified in this feature diff and has 694 lines. The repository general code-change policy limits production code, test code, and reusable scripts to 500 lines. The specification's AC25 no-growth statement is not an approved policy exception. + +## Required Fixes + +1. Split `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` into cohesive MSTest source files so every resulting modified or new test file is at or below 500 lines. + - Preserve all existing test method names, test behavior, assertions, and setup/cleanup semantics. + - Keep the Issue #637-related provider lookup assertion and the AC21 archive-relative selected-value correction intact. + - Do not change the production normalization behavior. +2. Update `QuickFiler.Test/QuickFiler.Test.csproj` compile includes for every new test source file created by the split. +3. Re-measure the affected test-file line counts and record evidence under the active feature's `evidence/qa-gates/` directory. +4. Rerun the full C# toolchain in order after the split: + - `dotnet tool run csharpier format .` + - `dotnet tool run csharpier check .` + - `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + - `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + - `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-remediation.cobertura.xml` +5. Refresh PR context after the remediation commit and repeat feature review against `main`. + +## Do Not Do + +- Do not weaken, delete, skip, or rename test methods merely to reduce line count. +- Do not alter `spec.md` AC21 wording or reclassify its deliberate specification correction as a weakened test. +- Do not modify production behavior outside the fixture split and necessary project compile includes. +- Do not weaken repository policy or introduce an exception for the 500-line limit. +- Do not bypass the required formatter, analyzer, nullable, test, coverage, PR-context, or review gates. + +## Required Context Package + +- `artifacts/pr_context.summary.txt` +- `artifacts/pr_context.appendix.txt` +- `policy-audit.2026-08-31T13-32.md` +- `code-review.2026-08-31T13-32.md` +- `feature-audit.2026-08-31T13-32.md` +- `plan.2026-08-29T12-20.md` +- `spec.md` diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-plan.2026-08-31T13-33.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-plan.2026-08-31T13-33.md new file mode 100644 index 000000000..341365d31 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-plan.2026-08-31T13-33.md @@ -0,0 +1,47 @@ +# Issue #637 Remediation Plan — split oversized Issue #439 test fixture + +## Authority and constraints + +- Requirements source: `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/remediation-inputs.2026-08-31T13-32.md`. +- Review inputs: `policy-audit.2026-08-31T13-32.md`, `code-review.2026-08-31T13-32.md`, and `feature-audit.2026-08-31T13-32.md` in the same feature folder. +- Product acceptance source: `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md`; AC21 remains the deliberate archive-relative selected-value specification correction and is not edited by this remediation. +- Reference plan and PR context: `plan.2026-08-29T12-20.md`, `artifacts/pr_context.summary.txt`, and `artifacts/pr_context.appendix.txt`. +- Scope is limited to splitting `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, adding the required non-SDK compile include in `QuickFiler.Test/QuickFiler.Test.csproj`, and canonical remediation evidence. Do not alter production code or behavior, delete or rename test methods, weaken assertions, alter setup/cleanup semantics, or edit `spec.md`. +- All new evidence paths below are canonical paths under `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/<kind>/`. + +### Phase 0 — Policy and remediation baseline + +- [x] [P0-T1] Read `AGENTS.md` standing, general code-change, and general unit-test instructions, then `.agents/skills/csharp/SKILL.md`; write `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t1-policy-read.md` with `Timestamp:`, `Policy Order:`, and the exact files read. Acceptance: the artifact records the required order and explicitly identifies the 500-line limit, MSTest/Moq/FluentAssertions requirements, and no-temporary-file rule. +- [x] [P0-T2] Run `dotnet tool run csharpier check .` before source changes and write its separate result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t2-csharpier-check.md`. Acceptance: the artifact contains `Timestamp:`, the exact `Command:`, `EXIT_CODE:`, and `Output Summary:`. +- [x] [P0-T3] Run `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` before source changes and write its separate result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t3-msbuild-analyzers.md`. Acceptance: the artifact contains `Timestamp:`, the exact `Command:`, `EXIT_CODE:`, and `Output Summary:`. +- [x] [P0-T4] Run `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` before source changes and write its separate result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t4-msbuild-nullable.md`. Acceptance: the artifact contains `Timestamp:`, the exact `Command:`, `EXIT_CODE:`, and `Output Summary:`. +- [x] [P0-T5] Preserve `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage.md` as the incomplete historical baseline attempt. Do not rewrite it or infer an elapsed duration, process ID, or termination detail that the historical artifact does not establish. Then perform exactly one bounded recovery with fresh evidence: before source changes, verify that no descendant process from the new wrapper invocation remains after it ends, then rerun the unchanged prescribed command `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\remediation-baseline.cobertura.xml` in one persistent terminal session without changing runsettings, worker count, timeout, filter, wrapper, test-assembly discovery, or coverage configuration. Apply an external 15-minute wall-clock ceiling to the new invocation without changing the wrapper, settings, workers, timeouts, filter, discovery, or coverage configuration. Write the fresh result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t5-mstest-coverage-retry.md` with `Timestamp:`, the exact unchanged `Command:`, `EXIT_CODE:`, `Output Summary:`, and the Cobertura output status; record any duration, process identity, or owned-process termination only when observed in this retry. On expiry, record the new invocation's wrapper and descendant process identities and command lines, terminate only descendants attributable to that invocation, verify cleanup, leave P0-T5 unchecked, and return a baseline-coverage blocker. If the bounded retry does not exit `0` and create `coverage\remediation-baseline.cobertura.xml`, leave P0-T5 unchecked and return a baseline-coverage blocker; do not retry, skip, weaken the coverage gate, or begin P0-T6. Acceptance: P0-T5 is checked only if the retry exits `0`, creates `coverage\remediation-baseline.cobertura.xml`, and its fresh evidence artifact contains `Timestamp:`, the exact unchanged `Command:`, `EXIT_CODE: 0`, `Output Summary:`, numeric repository line-coverage percentage, passing/failing test totals, and the generated Cobertura path. +- [x] [P0-T6] Record the pre-split target structure in `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/remediation-baseline/p0-t6-issue439-fixture-inventory.md` by capturing `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` line count, all ten fully qualified test method identities, its three private helper signatures (`Key`, `Chain`, and `Segment`), and its current explicit `Compile Include` line in `QuickFiler.Test/QuickFiler.Test.csproj`. Acceptance: the artifact records 694 lines and a complete ten-method identity list that becomes the post-split comparison authority. + +### Phase 1 — Cohesive fixture split + +- [x] [P1-T1] Write a split-map artifact at `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p1-t1-issue439-split-map.md` that assigns the four contiguous activation tests `Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget`, `Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget`, `Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget`, and `Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild` to `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs`; retain the other six Issue #439 tests and `Key`, `Chain`, and `Segment` helpers in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`. Acceptance: the map names all ten methods exactly once and explains that both files form the same partial MSTest class so method identities remain unchanged. +- [x] [P1-T2] Update `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` by changing only its class declaration to `partial` and removing exactly the four activation test method blocks identified by `evidence/other/p1-t1-issue439-split-map.md`; retain namespace, `[TestClass]`, the six remaining test methods, and the existing private helpers unchanged. Acceptance: the file contains no moved method, retains the corrected `router.SelectedFolderPath.Should().Be(@"Clients\North");` assertion and its rooted `ResolveLeafKeyAsync(fullTarget, ...)` verification, and has no behavior-bearing change outside the partial declaration and moved blocks. +- [x] [P1-T3] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` in namespace `QuickFiler.Test.Controllers` with the required `using` directives and a `partial` declaration of `BreadcrumbBridgeRouterIssue439Tests`; place exactly the four activation test methods identified in `evidence/other/p1-t1-issue439-split-map.md` in their original source order without changing their names, bodies, assertions, Moq setup, or cleanup semantics. Acceptance: each moved test compiles against the helpers retained in the original partial file and no test method receives a new class-qualified identity. +- [x] [P1-T4] Add exactly one explicit non-SDK project item `<Compile Include="Controllers\BreadcrumbBridgeRouterIssue439Tests.Activation.cs" />` adjacent to the existing Issue #439 compile include in `QuickFiler.Test/QuickFiler.Test.csproj`. Acceptance: each Issue #439 source file is included once, no existing `Compile Include` entry is removed or renamed, and `QuickFiler.Test/QuickFiler.Test.csproj` remains well-formed XML. +- [x] [P1-T5] Write `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p1-t5-fixture-split-verification.md` after independently comparing the pre-split inventory to the split files. Acceptance: it records the line count for both `BreadcrumbBridgeRouterIssue439Tests.cs` and `BreadcrumbBridgeRouterIssue439Tests.Activation.cs`, each at or below 500; lists all ten fully qualified test method identities once with no additions/removals/renames; records one `Compile Include` per source file; and verifies AC21 wording in `spec.md` is byte-for-byte unchanged. + +### Phase 2 — Post-split C# QA and coverage + +- [x] [P2-T1] Run `dotnet tool run csharpier format .` and write its result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t1-csharpier-format.md`. Acceptance: the artifact contains `Timestamp:`, the exact `Command:`, `EXIT_CODE:`, and `Output Summary:`; if formatting changes any file or any Phase 2 command fails, restart this entire Phase 2 sequence from P2-T1 after correcting the failure. +- [x] [P2-T2] Run `dotnet tool run csharpier check .` and write its result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t2-csharpier-check.md`. Acceptance: the artifact contains `Timestamp:`, the exact `Command:`, `EXIT_CODE: 0`, and `Output Summary:`; restart Phase 2 from P2-T1 if this task fails. +- [x] [P2-T3] Run `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` and write its result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t3-msbuild-analyzers.md`. Acceptance: the artifact contains `Timestamp:`, the exact `Command:`, `EXIT_CODE: 0`, and `Output Summary:`; restart Phase 2 from P2-T1 if this task fails. +- [x] [P2-T4] Run `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` and write its result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t4-msbuild-nullable.md`. Acceptance: the artifact contains `Timestamp:`, the exact `Command:`, `EXIT_CODE: 0`, and `Output Summary:`; restart Phase 2 from P2-T1 if this task fails. +- [x] [P2-T5] Run `pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-remediation.cobertura.xml` and write its result to `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t5-mstest-coverage.md`. Acceptance: the artifact contains `Timestamp:`, the exact `Command:`, `EXIT_CODE: 0`, `Output Summary:`, numeric repository line coverage, test totals, and the Cobertura output path; restart Phase 2 from P2-T1 if this task fails. +- [x] [P2-T6] Write `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t6-coverage-comparison.md` from the two coverage outputs. Acceptance: it records baseline and post-change numeric repository line coverage, verifies the post-change value is at least the repository 80% floor and does not regress from baseline, and records that this remediation changes only test/project surfaces so there are no changed production-code sequence points to calculate. +- [x] [P2-T7] Write `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/qa-gates/p2-t7-remediation-qa-audit.md`. Acceptance: it enumerates P2-T1 through P2-T5 in order, cites each corresponding artifact, confirms all final-pass exit codes are zero, records the file-size evidence from P1-T5, and states no planned command was skipped. + +### Phase 3 — Scope and review handoff + +- [x] [P3-T1] Write `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t1-remediation-scope-audit.md` from the remediation diff. Acceptance: the audit proves changed implementation paths are limited to `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs`, `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs`, and `QuickFiler.Test/QuickFiler.Test.csproj`; it separately lists remediation-plan/evidence updates; and it confirms no production file or `spec.md` change occurred. +- [x] [P3-T2] Record the executor terminal state in `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/evidence/other/p3-t2-remediation-commit-handoff.md`. Acceptance: it cites P1-T5, P2-T7, and P3-T1, identifies the exact files that the orchestrator must stage for the remediation commit, and returns `PROGRESS_COMMIT_REQUIRED: remediation-fixture-split` without creating a commit. + +### Phase 4 — Post-commit PR context and feature re-review + +- [ ] [P4-T1] After the orchestrator has created the remediation commit, refresh `artifacts/pr_context.summary.txt` and `artifacts/pr_context.appendix.txt` against base `main` using the repository PR-context workflow. Acceptance: both artifacts identify `main` as the resolved base, the remediation commit as part of the head range, and the active #637 feature folder as context. +- [ ] [P4-T2] Delegate a complete feature review against `main` using the refreshed PR context, `remediation-inputs.2026-08-31T13-32.md`, the three prior audit artifacts, `spec.md`, and this plan. Acceptance: the resulting policy, code, and feature audit artifacts mark the 500-line fixture finding resolved, retain AC21 as a deliberate specification correction, and return no blocking remediation finding; otherwise return remediation-required to the orchestrator for a new bounded remediation cycle. From 16ad2da0c71e4124dee5ddf8253aa9e0cd3386d6 Mon Sep 17 00:00:00 2001 From: Dan Moisan <drmoisan@gmail.com> Date: Mon, 31 Aug 2026 13:27:09 -0400 Subject: [PATCH 16/16] docs(issue-637): record passing post-remediation audits - Record code, feature, and policy audit findings with no blocking issues - Preserve evidence for full C# QA, coverage, and AC21 validation Refs: #637 --- .../code-review.2026-08-31T13-20.md | 31 +++++ .../feature-audit.2026-08-31T13-20.md | 40 +++++++ .../policy-audit.2026-08-31T13-20.md | 110 ++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-20.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-20.md create mode 100644 docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-20.md diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-20.md new file mode 100644 index 000000000..4c7289c2e --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/code-review.2026-08-31T13-20.md @@ -0,0 +1,31 @@ +# Code Review: Issue #637 rooted breadcrumb selection normalization + +## Executive Summary + +The review of `main...952a760fb19ff9c10007fe2ebb42f8cadd49a886` found no blocking code defect. `SelectRow` normalizes only full paths with a bound root and preserves all specified non-selection and pass-through behavior. The `MoveToFolderAsync(string, ...)` assignment uses a total helper that leaves ineligible values verbatim. The post-remediation Issue #439 fixture split preserves the existing test identities and compile items while resolving the prior file-size blocker. + +## Findings Table + +| Severity | File | Location | Finding | Recommendation | Rationale | Evidence | +|---|---|---|---|---|---|---| +| None | N/A | N/A | No code-level blocking finding identified. | No remediation required. | The reviewed implementation, test split, static checks, and exact-head QA evidence satisfy the feature requirements. | `git diff --check main...HEAD`; live CSharpier check; `p1-t5` and `p2-t1` through `p2-t7` evidence. | + +## Reviewed Implementation + +- `BreadcrumbBridgeRouter.Selection.SelectRow` keeps the no-bound-root pass-through, rejects out-of-root full paths, treats archive-root-exact as a deterministic non-selection, and commits a non-empty archive-relative stem for eligible full paths. +- `EfcDataModel.ToFilingStemOrVerbatim` is an internal pure helper. It returns a non-empty archive-relative stem only when `TryMakeArchiveRelative` succeeds; relative, sentinel, root-exact, outside-root, null, and invalid-ancestor inputs remain verbatim without throwing. +- `MoveToFolderAsync(string, ...)` applies the helper only to `DestinationOlStem`; the two folder-opening paths retain their existing assignments. +- The Issue #439 selected-path assertion now expects `Clients\\North`, while the provider lookup remains rooted. This preserves AC21's explicit invariant-driven deliberate specification correction rather than weakening coverage. +- The formerly oversized Issue #439 test fixture is split into a 455-line retained partial and a 253-line activation partial. The project lists both compilation items once, and the named test inventory is preserved. + +## Verification Evidence + +- Fresh PR context resolves `main` at `3be3f237a8551df3f27f83d9d1af2f26074fc93a` and HEAD at `952a760fb19ff9c10007fe2ebb42f8cadd49a886`. +- `git diff --check main...HEAD` passed during this review. +- `dotnet tool run csharpier check .` passed during this review (1,565 files checked). +- Current-head QA artifacts record formatter, analyzer, nullable, and MSTest coverage success in order: 6,894 passed, 0 failed, and 85.3389% repository line coverage. +- The exact scope evidence enumerates only the Issue #637 product paths; the post-remediation commit changes the planned test/project surfaces and review/evidence artifacts. + +## Conclusion + +No blocking code-review finding remains. The fixture-size remediation is verified without altering product behavior or the AC21 deliberate specification-correction wording. diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-20.md new file mode 100644 index 000000000..3d3b78d42 --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/feature-audit.2026-08-31T13-20.md @@ -0,0 +1,40 @@ +# Feature Audit: Issue #637 rooted breadcrumb selection normalization + +## Scope and Baseline + +This post-remediation feature audit compares HEAD `952a760fb19ff9c10007fe2ebb42f8cadd49a886` with `main` at merge base `3be3f237a8551df3f27f83d9d1af2f26074fc93a`. Fresh `artifacts/pr_context.summary.txt` and `artifacts/pr_context.appendix.txt` identify the same resolved base, head, and active feature folder: `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637`. + +The feature range changes C#, project, Markdown, and agent-memory documents. The executable implementation scope is the ten Issue #637 QuickFiler and QuickFiler.Test paths recorded in `evidence/qa-gates/p8-t30-scope-boundary.md`. The final remediation commit adds the partial test fixture and its project include; it leaves `spec.md`, including AC21, unchanged. + +## Acceptance Criteria Inventory + +The persisted work mode in `issue.md` is `full-bug`, so `spec.md` is the authoritative acceptance-criteria source. Its scoped acceptance-criteria section contains 30 checkbox items: 30 checked and zero unchecked, independently confirmed by `p8-t31-ac-reconciliation.md`. + +## Acceptance Criteria Evaluation + +| Criteria | Status | Evidence | +|---|---|---| +| AC1-AC5 | PASS | Issue #637 tests and source review verify root-exact non-selection, under-root stem commitment, full-path-only nesting, no-bound-root pass-through, and Trash preservation. | +| AC6-AC10 | PASS | Out-of-root and boundary rejection, value-free root-exact diagnostic, unchanged hierarchy behavior, selection-family shape, and common `SelectRow` reachability are covered by tests and Phase 8 evidence. | +| AC11-AC14 | PASS | `EfcDataModel.FilingStem.cs` supplies the internal pure helper; Issue #637 helper tests cover rooted, verbatim, and total/no-throw cases. | +| AC15-AC17 | PASS | Existing archive-stem behavior, MoveToFolder family shape, and the excluded folder-opening paths remain unchanged. | +| AC18-AC21 | PASS | The corrected Issue #439 selection assertion retains the rooted provider lookup. `p8-t21-spec-correction-record.md` and unchanged `spec.md` preserve the deliberate, invariant-driven specification-correction statement. | +| AC22-AC24 | PASS | The three stale deferral records were corrected while guard behavior and the selected-folder-path surface remain unchanged. | +| AC25 | PASS | Remediation split the modified Issue #439 fixture into 455- and 253-line partial files, both within the repository maximum. | +| AC26-AC30 | PASS | Each partial appears once in the test project; nullable, ordered toolchain, coverage, and full scope boundary evidence pass. | + +## Summary + +All 30 product acceptance criteria are met, and the post-remediation audit found no policy or code blocker. The fixture split resolves the prior policy failure while preserving the original assertions, test identities, and AC21 deliberate specification-correction language. + +## Acceptance Criteria Check-off + +No acceptance checkbox was changed by this review. All authoritative `spec.md` acceptance criteria were already checked and each evaluated PASS. + +### Acceptance Criteria Status + +- Source: `docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/spec.md` +- Total AC items: 30 +- Checked off (delivered): 30 +- Remaining (unchecked): 0 +- Items remaining: none diff --git a/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-20.md b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-20.md new file mode 100644 index 000000000..ec6ac1ecc --- /dev/null +++ b/docs/features/active/2026-08-26-breadcrumb-selectrow-emits-rooted-path-leaving-d1-half-closed-637/policy-audit.2026-08-31T13-20.md @@ -0,0 +1,110 @@ +# Policy Compliance Audit: Issue #637 rooted breadcrumb selection normalization + +**Audit Date:** 2026-08-31 +**Base / Head:** `main` (`3be3f237a8551df3f27f83d9d1af2f26074fc93a`) / `952a760fb19ff9c10007fe2ebb42f8cadd49a886` + +| Language | Files changed | Tests | Result | Baseline line coverage | Post-change line coverage | Changed/new code coverage | +|---|---:|---:|---|---:|---:|---| +| C# | 9 | 6,894 | PASS, 6,894 passed and 0 failed | 85.3358% | 85.3389% | 100.0% — initial feature changed-line evidence covers the new helper; post-split remediation changes only test/project paths | +| MSBuild XML | 2 | N/A | PASS; analyzer and nullable rebuilds passed | N/A | N/A | N/A | +| Markdown | 108 | N/A | PASS; `git diff --check main...HEAD` passed | N/A | N/A | N/A | + +### Coverage Evidence Checklist + +- TypeScript baseline coverage artifact: `N/A - zero TypeScript files changed in main...HEAD` +- TypeScript post-change coverage artifact: `N/A - zero TypeScript files changed in main...HEAD` +- PowerShell baseline coverage artifact: `N/A - zero PowerShell files changed in main...HEAD` +- PowerShell post-change coverage artifact: `N/A - zero PowerShell files changed in main...HEAD` +- Per-language comparison summary: `### 1.2.1 Per-Language Coverage Comparison` + +### 1.2.1 Per-Language Coverage Comparison + +- C#: Baseline: 85.3358% lines -> Post-change: 85.3389% lines. Change: +0.0031 percentage points. New/changed-code coverage: 100.0% for the initial helper lines; the post-split remediation changed no production-code sequence points. Disposition: PASS. Evidence: `evidence/qa-gates/p2-t5-mstest-coverage.md`, `evidence/qa-gates/p2-t6-coverage-comparison.md`, and `evidence/qa-gates/p7-t7-changed-line-coverage.md`. +- TypeScript: zero changed files. Disposition: N/A. +- PowerShell: zero changed files. Disposition: N/A. + +## Executive Summary + +This post-remediation audit reviewed the full `main...HEAD` feature range using fresh canonical PR context. The remediation splits the previously oversized Issue #439 test fixture into two cohesive partial files: 455 and 253 logical lines, respectively. Both are below the repository 500-line maximum. Live `git diff --check main...HEAD` and CSharpier check passed; current-head QA evidence records successful analyzer, nullable, and MSTest-with-coverage commands. No policy blocker remains. + +## 1. General Unit Test Policy Compliance + +| Requirement | Status | Evidence | +|---|---|---| +| Independence and isolation | PASS | New rooted-selection tests use existing `IFolderHierarchyProvider` and `IBreadcrumbWebHost` Moq seams and detach the in-memory log appender in cleanup. | +| Determinism and test outcomes | PASS | `p2-t5-mstest-coverage.md` records 6,894 passed and 0 failed across 9 assemblies. | +| Positive, negative, boundary, and error scenarios | PASS | Issue #637 tests cover root-exact, under-root, case/trailing-separator, relative, Trash, out-of-root, separator near-miss, no-bound-root, and `SelectFirstRow`; helper tests cover root-exact, out-of-root, null/empty, and ancestor edge cases. | +| External dependencies and temporary files | PASS | Reviewed tests use mocks or pure static calls; no test temporary-file or external-service dependency was introduced. | +| Readability and maintainability | PASS | The remediated Issue #439 fixture is now split into two partial files below 500 lines. | + +## 2. General Code Change Policy Compliance + +| Requirement | Status | Evidence | +|---|---|---| +| Objective and plan | PASS | `issue.md`, `spec.md`, research, original atomic plan, and remediation plan define the completed scope. | +| Simplicity and separation of concerns | PASS | `SelectRow` normalizes only eligible full paths; a pure internal helper isolates `MoveToFolderAsync(string, ...)` normalization. | +| Naming, contracts, and comments | PASS | `ToFilingStemOrVerbatim` documents its verbatim/no-throw contract; stale deferral records now state that producer normalization is implemented. | +| Cohesive modules and 500-line maximum | PASS | Source and remediated test fixture logical line counts are within policy: 455 and 253 for the two Issue #439 partial fixtures. | +| Toolchain execution | PASS | The ordered current-head evidence set records CSharpier format/check, analyzer rebuild, nullable rebuild, and MSTest coverage as successful. | + +## 3. Language-Specific Code Change Policy Compliance + +### C# + +| Requirement | Status | Evidence | +|---|---|---| +| CSharpier formatting | PASS | Live `dotnet tool run csharpier check .` checked 1,565 files without a violation; `p2-t1` format also exited 0. | +| Analyzer diagnostics | PASS | `p2-t3-msbuild-analyzers.md` records a successful analyzer-enabled rebuild with 0 errors. | +| Nullable posture | PASS | `p2-t4-msbuild-nullable.md` records a successful warnings-as-errors rebuild with 0 errors. | +| Focused design and visibility | PASS | The new helper is `internal static` on a partial `EfcDataModel`; no public API was added. | + +## 4. Language-Specific Unit Test Policy Compliance + +| Requirement | Status | Evidence | +|---|---|---| +| MSTest framework | PASS | Changed tests use `[TestClass]` and `[TestMethod]`. | +| Moq and FluentAssertions | PASS | Router coverage uses Moq seams and FluentAssertions; pure helper tests use FluentAssertions. | +| File-size limit for modified tests | PASS | `p1-t5-fixture-split-verification.md` records 455 and 253 logical lines; direct post-remediation count confirmed the same files remain below 500. | + +## 5. Test Coverage Detail + +The current-head post-split coverage artifact (`evidence/qa-gates/p2-t5-mstest-coverage.md`) records 85.3389% repository line coverage, exceeding the 80% floor, with 6,894 passed and 0 failed. `p2-t6-coverage-comparison.md` records a +0.0031 percentage-point change from the remediation baseline and no changed production-code sequence points for the fixture-only remediation. Earlier feature evidence records coverage for the new filing-stem helper; the remediation did not alter production code. + +## 6. Test Execution Metrics + +The current-head coverage wrapper discovered nine test assemblies, completed successfully, and reported 6,894 total tests passed with zero failures. Analyzer and nullable rebuilds each completed with zero errors; the evidence retains five pre-existing `System.Reactive` packages.config compatibility warnings from unrelated projects. + +## 7. Code Quality Checks + +`git diff --check main...HEAD` exited 0 during this review. `dotnet tool run csharpier check .` completed successfully during this review. The exact-head QA evidence additionally records successful format, analyzer, nullable, and coverage commands in required order. + +## 8. Gaps and Exceptions + +No policy exception is required. The prior 694-line modified test-fixture finding is resolved by the partial-fixture split. The authoritative `spec.md` remains unchanged by that remediation, including AC21's deliberate specification-correction language. + +## 9. Summary of Changes + +The feature normalizes rooted Outlook paths in `SelectRow`, uses a total internal filing-stem helper for the string `MoveToFolderAsync` path, corrects the superseded rooted-selection assertion without changing its rooted provider-lookup assertion, updates stale deferral records, and remediates the test fixture-size policy finding by splitting the Issue #439 tests. + +## 10. Compliance Verdict + +PASS — the full feature range satisfies the reviewed repository policy requirements. The post-remediation fixture split removes the only prior blocking finding, while the feature's behavior, AC21 invariant-driven specification correction, and QA evidence remain intact. + +## Appendix A: Test Inventory + +- `BreadcrumbBridgeRouterIssue637Tests`: rooted-path selection coverage. +- `EfcDataModelIssue614Tests`: archive-stem and helper coverage. +- `BreadcrumbBridgeRouterIssue439Tests` and `.Activation`: corrected expected selection and retained provider-lookup assertion. +- `EfcSelectionGuardTests`: unchanged behavior with updated stale-deferral wording. + +## Appendix B: Toolchain Commands Reference + +`dotnet tool run csharpier format .` + +`dotnet tool run csharpier check .` + +`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + +`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + +`pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput coverage\p7-t5-remediation.cobertura.xml`