From 17bfc0cdb9cc1988c604f1b37b65a584f8fd165c Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sun, 6 Sep 2026 22:11:04 -0400 Subject: [PATCH 01/13] docs(799): promote breadcrumb archive-root lineage bug to an active folder Records the promoted potential entry and seeds the active bug folder for issue #799 in full-bug work mode. The promoted record carries the eight acceptance criteria AC1-AC8 settled with the maintainer on 2026-09-06 verbatim, and issue.md carries the full-bug work-mode marker. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MS8fb2wX1Sx1bG67LAr41e --- .../issue.md | 109 +++++++++++ .../plan.2026-09-06T22-01.md | 44 +++++ .../spec.md | 171 ++++++++++++++++++ ...ve-root-and-suggestion-path-consistency.md | 107 +++++++++++ 4 files changed, 431 insertions(+) create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/issue.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md create mode 100644 docs/features/potential/promoted/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency.md diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/issue.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/issue.md new file mode 100644 index 000000000..920724159 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/issue.md @@ -0,0 +1,109 @@ +# breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency (Issue #799) + +- Date captured: 2026-09-06 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency/ (Issue #799) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #799 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/799 +- Last Updated: 2026-09-07 +- Work Mode: full-bug + +## Summary + +In QuickFiler (ordinary and High Confidence modes) suggestion rows render their breadcrumb lineage from the store root, for example `dmoisan@realgoodfoods.com -> Archive -> _Active Projects -> Build RGF Org and Team -> Sales Lead`, while typed search-result rows render `_Active Projects -> Build RGF Org and Team -> Sales Lead`. The mailbox and Archive segments are superfluous: every filing target is under the archive root, and the lineage must begin at the first segment below it for every row kind. The full-root lineage is the behavior delivered by issue #439 ("full root-to-leaf ancestor lineage"), so this is a specification change, not a regression. The same change should unify several archive-root stripping paths that have drifted into duplicates and gaps. + +## Environment + +- OS/version: Windows 11 Pro 10.0.26200 +- Runtime: .NET Framework 4.8 VSTO Outlook add-in with WebView2 breadcrumb, debug build from `TaskMaster\bin\Debug`, HEAD `c431dc32` (2026-09-06) +- Command/flags used: Outlook ribbon -> QuickFiler and QuickFiler High Confidence; folder drop-down in the item view (`ItemViewer` breadcrumb, `FolderBreadcrumb.html`) +- Data source or fixture: live mailbox; archive root resolves to `\\dmoisan@realgoodfoods.com\Archive` (`AppOlObjects.ArchiveRootPath`) + +## Steps to Reproduce + +1. Launch QuickFiler on Inbox. Open the folder drop-down on an item without typing. +2. Observe the first suggestion row: `dmoisan@realgoodfoods.com -> Archive -> _Active Projects -> Build RGF Org and Team -> Sales Lead`. +3. Type a few letters into the search box. Observe the same folder as a search result: `_Active Projects -> Build RGF Org and Team -> Sales Lead`. Typing `90` shows `_Active Projects -> 90 Day Plan`. +4. Accept a suggestion: filing lands in the correct Outlook and file-system folders (the filing target is the archive-relative stem, not the displayed lineage). + +Note: the maintainer's transcription of the search row showed `_ Active Projects` with a space after the underscore. The orchestrator should verify whether the renderer alters a leading underscore or whether this was a transcription artifact. + +## Expected Behavior + +- Every row in the folder list, suggestion or search result, renders as `_Active Projects -> Build RGF Org and Team -> Sales Lead`: the lineage begins at the first segment below the archive root, each segment is clickable for ancestor navigation, and no row shows the mailbox or Archive segments. +- A resolved ancestor chain that does not pass through the archive root node is logged as an error and rendered with the existing single-segment fallback, never with a mailbox prefix. There are no legitimate filing targets outside the archive root. +- One archive-root projection rule is used everywhere a suggestion path is prepared for display or compared against displayed entries. + +## Actual Behavior + +- Suggestion rows show the mailbox and Archive segments; search rows do not. Ordinary and High Confidence modes behave the same. +- Recent-folder entries are appended to the suggestion list with no projection at all. +- The Efc breadcrumb binding joins projected row text against raw scorer paths, so an archive-rooted suggestion loses its percentage. +- Several suggestion rows fail hierarchy resolution and fall back to a single segment, with errors such as `No snapshot node path ends with '\Forums\Pricing'; leaving 'Forums\Pricing' unresolved.` + +## Logs / Screenshots + +- [x] Attached minimal logs or screenshot +- Snippet (`TaskMaster\bin\Debug\logs\debug_2026-09-06.log`), stale-label resolution failures: + +``` +2026-09-06 16:45:51,908 [VSTA_Main] ERROR UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider - No snapshot node path ends with '\Scorecards\Monthly Scans'; leaving 'Scorecards\Monthly Scans' unresolved. +2026-09-06 16:45:51,937 [VSTA_Main] ERROR UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider - No snapshot node path ends with '\Forums\Pricing'; leaving 'Forums\Pricing' unresolved. +``` + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +Display-only: filing lands correctly. The redundant segments consume most of the row width and make same-named folders harder to distinguish, which is the problem #439 set out to solve. + +## Suspected Cause / Notes + +Why the two row kinds differ (verified by code read): + +- Suggestion rows: `FolderBreadcrumbBridgeRouter.SetSuggestionsAsync` decorates each row with the ancestor chain from `IFolderHierarchyProvider`. `FolderTreeSnapshotQueries.GetAncestorChain` (`UtilitiesCS\OutlookObjects\Folder\FolderTreeSnapshotQueries.cs:109-140`) walks `ParentKey` until it is null, i.e. up to the store node. `BreadcrumbRowBuilder.MapSegments` (`UtilitiesCS\OutlookObjects\Folder\BreadcrumbRowBuilder.cs:178-208`) maps every node to a segment with no trimming. This is the #439 design (`docs\features\active\2026-08-07-efcviewer-missing-lineage-and-segment-navigation-439\issue.md`, Expected Behavior). +- Search rows: `FolderBreadcrumbBridgeRouter.ReplaceItemsPreservingSession` (`FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55`) carries the verbatim archive-relative stem produced by `FolderPredictor.GetOlSubpath` (`FolderPredictor.cs:953-971`), and `BreadcrumbRenderProjection.SplitVerbatim` (`BreadcrumbRenderProjection.cs:242-246`) splits it on path separators for rendering. No provider chain, so no store segments. + +Archive-root projection paths that should be unified under `ArchiveStemContract` (`UtilitiesCS\OutlookObjects\Folder\ArchiveStemContract.cs`, the canonical ordinal, anchored, separator-terminated stripper from #614): + +- `FolderPredictor.ProjectSuggestionPath` (`FolderPredictor.cs:848-861`): private, backslash-only, appends `"\\"` to the root unconditionally, silently returns the full path on no match, and with an empty root strips one leading separator from any path. +- `QfcItemController.ProjectPredeterminedFolder` (`QuickFiler\Controllers\QfcItemController.FolderHandling.cs:272-285`): a hand-copied duplicate of the above added by #678 AC12 because the original is private. +- `FolderPredictor.AddRecents` (`FolderPredictor.cs:788-795`): appends `_globals.AF.RecentsList` verbatim with no projection. +- `FolderPredictor.GetOlSubpath` (`FolderPredictor.cs:953-971`): blind `Substring` with no prefix verification. +- `FolderMinimalWrapper.ToRelativePath` (`FolderMinimalWrapper.cs:84`) and `FolderWrapper.RelativePath` (`FolderWrapper .cs:194-224`): unanchored `Replace(root + "\\", "")` with a full-path fallback when the root is null, which is how a rooted label can enter the persisted classifier corpus. +- `EfcFormController.BindBreadcrumbRowsAsync` (`QuickFiler\Controllers\EfcFormController.cs:1115-1118`): passes projected `rows` with raw `Suggestions.ToScoredArray()` scores to `BindRowsAsync`, which joins by presented-text equality, so archive-rooted suggestions lose their score. `AddSuggestionRows` (`FolderPredictor.cs:835-846`) projects the score path; this site does not. +- `ToDoModel\Email Utilities\SortItemsToExistingFolder.cs:67-109`: legacy unanchored `Contains(StrRoot)` and `Substring` root handling. + +Stale labels: the resolution failures logged above come from `OutlookFolderHierarchyProvider.ResolveByUniqueSuffix` (`OutlookFolderHierarchyProvider.cs:85-112`) when a persisted suggestion label (classifier corpus, subject map, or recents) names a folder that no longer exists at that path. Those rows fall back to a single segment. The change should make the fallback visibly distinct (or filter such labels) and log once per label rather than once per render. + +Runtime facts that constrain the design: `AppOlObjects.ArchiveRootPath` (`TaskMaster\AppGlobals\AppOlObjects.cs:260-270`, `AppOlObjects.ArchiveRoot.cs:86-93`) is derived from the default store root plus the literal folder `Archive`, validated by `ArchiveRootPathGuard`, and throws rather than returning null. Persisted suggestion labels are archive-relative when written correctly (`OlFolderClassifierGroup.cs:205`, `SubjectMapSco.Orchestration.cs:19-33`, `AppAutoFileObjects.cs:211-229`). + +## Proposed Fix / Validation Ideas + +Acceptance criteria settled with the maintainer on 2026-09-06: + +- [ ] AC1: Suggestion rows and search-result rows in both the QuickFiler item view and the Efc view render the lineage starting at the first segment below the archive root, with the same arrow rendering and clickable ancestor segments for both row kinds. Example: `_Active Projects -> Build RGF Org and Team -> Sales Lead`. +- [ ] AC2: A resolved chain that does not pass through the archive root node is logged as an error and rendered with the existing single-segment fallback; no row ever shows the mailbox or Archive segment. +- [ ] AC3: Filing target and score-lookup key remain the archive-relative stem (unchanged #439 constraint); filing to the selected folder still lands correctly. +- [ ] AC4: `ProjectSuggestionPath` and `ProjectPredeterminedFolder` are replaced by one shared projection built on `ArchiveStemContract.TryMakeArchiveRelative`, and the empty-root one-separator strip is eliminated. +- [ ] AC5: Recent-folder entries pass through the same projection before display. +- [ ] AC6: `EfcFormController.BindBreadcrumbRowsAsync` projects the score paths the same way as the rows, so archive-rooted suggestions retain their percentage. +- [ ] AC7: Persisted suggestion labels that fail hierarchy resolution are rendered distinguishably (or filtered) and logged once per label per session, not once per render. +- [ ] AC8: The leading-underscore rendering question (`_Active Projects` vs `_ Active Projects`) is verified and, if the renderer alters it, corrected. + +Validation: + +- [ ] Unit coverage areas: chain trimming below the archive root (chain through root, chain not through root, chain equal to root); shared projection against the #614 contract cases (case-insensitive, trailing separators, `Archive2` boundary); recents projection; Efc score join with rooted and relative paths; renderer segment text preservation for leading underscores. +- [ ] Integration scenario to retest: bind a row set with a suggestion, a search result, a `====` banner, the trash pseudo-row, and a stale label; assert lineage on both folder row kinds, fallback on the stale label, none on banner/trash. +- [ ] Manual verification notes: QuickFiler ordinary and High Confidence, plus Efc view: confirm no row begins with the mailbox or Archive, and that clicking a middle segment navigates to that ancestor. + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md new file mode 100644 index 000000000..902da72cd --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md @@ -0,0 +1,44 @@ +# 2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency (Plan) + +- **Issue:** #799 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-09-06T22-01 +- **Status:** Draft +- **Version:** 0.1 + +**Fail-closed evidence rule:** Include explicit baseline artifact tasks, final-QA artifact tasks, and coverage-comparison tasks for each in-scope language when policy requires coverage. If any required baseline artifact, QA artifact, or coverage-comparison artifact is missing, the audit verdict must be BLOCKED or INCOMPLETE, never PASS. + +**Evidence accounting rule:** Record the expected artifact path or location in each evidence-producing task. Do not mark evidence-backed work complete without the artifact. + + +**Phase 0 — Context & Inputs** +- [ ] [P0-T1] Link approved spec: +- [ ] [P0-T2] Record branch/commit baseline: +- [ ] [P0-T3] List required environment/fixtures/data: + +**Phase 1 — Preparation** +- [ ] [P1-T1] Confirm scope is locked for this fix (no open spec gaps) +- [ ] [P1-T2] Sync workspace to target branch and ensure tooling is available + +**Phase 2 — Regression Test (must fail first)** +- [ ] [P2-T1] [expect-fail] Add a small, deterministic regression test in the standard module file (use `tests/bugs//#799-.py` only if no clear home exists) +- [ ] [P2-T2] [expect-fail] Run the regression to confirm it fails and captures the repro + +**Phase 3 — Minimal Fix** +- [ ] [P3-T1] Apply the smallest change needed to make the regression test pass; avoid opportunistic refactors + +**Phase 4 — Verification Loop** +- [ ] [P4-T1] Re-run repro and regression test to confirm expected behavior +- [ ] [P4-T2] Run formatter → linter → type checker → tests; restart loop if any step changes files or fails +- [ ] [P4-T3] Record baseline, post-change, and comparison artifact paths for each in-scope language where coverage is required + +**Phase 5 — Documentation & Status** +- [ ] [P5-T1] Update spec/issue with outcomes, decisions, and any deviations from scope + +**Phase 6 — PR & Handoff** +- [ ] [P6-T1] Prepare PR notes (summary, risks, validation performed, links to tests) and request review + +**Phase 7 — Rollout / Follow-up** +- [ ] [P7-T1] Capture deployment/rollout notes and post-fix monitoring items +- [ ] [P7-T2] Record links (issue, PRs, related docs) for traceability diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md new file mode 100644 index 000000000..66dcc10ae --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md @@ -0,0 +1,171 @@ +# 2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency (Spec) + +- **Issue:** #799 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-09-06T22-01 +- **Status:** Draft +- **Version:** 0.1 + +## Context +In QuickFiler (ordinary and High Confidence modes) suggestion rows render their breadcrumb lineage from the store root, for example `dmoisan@realgoodfoods.com -> Archive -> _Active Projects -> Build RGF Org and Team -> Sales Lead`, while typed search-result rows render `_Active Projects -> Build RGF Org and Team -> Sales Lead`. The mailbox and Archive segments are superfluous: every filing target is under the archive root, and the lineage must begin at the first segment below it for every row kind. The full-root lineage is the behavior delivered by issue #439 ("full root-to-leaf ancestor lineage"), so this is a specification change, not a regression. The same change should unify several archive-root stripping paths that have drifted into duplicates and gaps. + +Environment: +- OS/version: Windows 11 Pro 10.0.26200 +- Runtime: .NET Framework 4.8 VSTO Outlook add-in with WebView2 breadcrumb, debug build from `TaskMaster\bin\Debug`, HEAD `c431dc32` (2026-09-06) +- Command/flags used: Outlook ribbon -> QuickFiler and QuickFiler High Confidence; folder drop-down in the item view (`ItemViewer` breadcrumb, `FolderBreadcrumb.html`) +- Data source or fixture: live mailbox; archive root resolves to `\\dmoisan@realgoodfoods.com\Archive` (`AppOlObjects.ArchiveRootPath`) + +Impact / Severity: +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +Display-only: filing lands correctly. The redundant segments consume most of the row width and make same-named folders harder to distinguish, which is the problem #439 set out to solve. + + +## Repro & Evidence +Steps to Reproduce: +1. Launch QuickFiler on Inbox. Open the folder drop-down on an item without typing. +2. Observe the first suggestion row: `dmoisan@realgoodfoods.com -> Archive -> _Active Projects -> Build RGF Org and Team -> Sales Lead`. +3. Type a few letters into the search box. Observe the same folder as a search result: `_Active Projects -> Build RGF Org and Team -> Sales Lead`. Typing `90` shows `_Active Projects -> 90 Day Plan`. +4. Accept a suggestion: filing lands in the correct Outlook and file-system folders (the filing target is the archive-relative stem, not the displayed lineage). + +Note: the maintainer's transcription of the search row showed `_ Active Projects` with a space after the underscore. The orchestrator should verify whether the renderer alters a leading underscore or whether this was a transcription artifact. + +Expected: +- Every row in the folder list, suggestion or search result, renders as `_Active Projects -> Build RGF Org and Team -> Sales Lead`: the lineage begins at the first segment below the archive root, each segment is clickable for ancestor navigation, and no row shows the mailbox or Archive segments. +- A resolved ancestor chain that does not pass through the archive root node is logged as an error and rendered with the existing single-segment fallback, never with a mailbox prefix. There are no legitimate filing targets outside the archive root. +- One archive-root projection rule is used everywhere a suggestion path is prepared for display or compared against displayed entries. + +Actual: +- Suggestion rows show the mailbox and Archive segments; search rows do not. Ordinary and High Confidence modes behave the same. +- Recent-folder entries are appended to the suggestion list with no projection at all. +- The Efc breadcrumb binding joins projected row text against raw scorer paths, so an archive-rooted suggestion loses its percentage. +- Several suggestion rows fail hierarchy resolution and fall back to a single segment, with errors such as `No snapshot node path ends with '\Forums\Pricing'; leaving 'Forums\Pricing' unresolved.` + +Logs / Screenshots: +- [x] Attached minimal logs or screenshot +- Snippet (`TaskMaster\bin\Debug\logs\debug_2026-09-06.log`), stale-label resolution failures: + +``` +2026-09-06 16:45:51,908 [VSTA_Main] ERROR UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider - No snapshot node path ends with '\Scorecards\Monthly Scans'; leaving 'Scorecards\Monthly Scans' unresolved. +2026-09-06 16:45:51,937 [VSTA_Main] ERROR UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider - No snapshot node path ends with '\Forums\Pricing'; leaving 'Forums\Pricing' unresolved. +``` + + +## Scope & Non-Goals +- In scope: +- Out of scope / non-goals: +- Explicitly excluded systems, integrations, or datasets: + +## Root Cause Analysis +Why the two row kinds differ (verified by code read): + +- Suggestion rows: `FolderBreadcrumbBridgeRouter.SetSuggestionsAsync` decorates each row with the ancestor chain from `IFolderHierarchyProvider`. `FolderTreeSnapshotQueries.GetAncestorChain` (`UtilitiesCS\OutlookObjects\Folder\FolderTreeSnapshotQueries.cs:109-140`) walks `ParentKey` until it is null, i.e. up to the store node. `BreadcrumbRowBuilder.MapSegments` (`UtilitiesCS\OutlookObjects\Folder\BreadcrumbRowBuilder.cs:178-208`) maps every node to a segment with no trimming. This is the #439 design (`docs\features\active\2026-08-07-efcviewer-missing-lineage-and-segment-navigation-439\issue.md`, Expected Behavior). +- Search rows: `FolderBreadcrumbBridgeRouter.ReplaceItemsPreservingSession` (`FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55`) carries the verbatim archive-relative stem produced by `FolderPredictor.GetOlSubpath` (`FolderPredictor.cs:953-971`), and `BreadcrumbRenderProjection.SplitVerbatim` (`BreadcrumbRenderProjection.cs:242-246`) splits it on path separators for rendering. No provider chain, so no store segments. + +Archive-root projection paths that should be unified under `ArchiveStemContract` (`UtilitiesCS\OutlookObjects\Folder\ArchiveStemContract.cs`, the canonical ordinal, anchored, separator-terminated stripper from #614): + +- `FolderPredictor.ProjectSuggestionPath` (`FolderPredictor.cs:848-861`): private, backslash-only, appends `"\\"` to the root unconditionally, silently returns the full path on no match, and with an empty root strips one leading separator from any path. +- `QfcItemController.ProjectPredeterminedFolder` (`QuickFiler\Controllers\QfcItemController.FolderHandling.cs:272-285`): a hand-copied duplicate of the above added by #678 AC12 because the original is private. +- `FolderPredictor.AddRecents` (`FolderPredictor.cs:788-795`): appends `_globals.AF.RecentsList` verbatim with no projection. +- `FolderPredictor.GetOlSubpath` (`FolderPredictor.cs:953-971`): blind `Substring` with no prefix verification. +- `FolderMinimalWrapper.ToRelativePath` (`FolderMinimalWrapper.cs:84`) and `FolderWrapper.RelativePath` (`FolderWrapper .cs:194-224`): unanchored `Replace(root + "\\", "")` with a full-path fallback when the root is null, which is how a rooted label can enter the persisted classifier corpus. +- `EfcFormController.BindBreadcrumbRowsAsync` (`QuickFiler\Controllers\EfcFormController.cs:1115-1118`): passes projected `rows` with raw `Suggestions.ToScoredArray()` scores to `BindRowsAsync`, which joins by presented-text equality, so archive-rooted suggestions lose their score. `AddSuggestionRows` (`FolderPredictor.cs:835-846`) projects the score path; this site does not. +- `ToDoModel\Email Utilities\SortItemsToExistingFolder.cs:67-109`: legacy unanchored `Contains(StrRoot)` and `Substring` root handling. + +Stale labels: the resolution failures logged above come from `OutlookFolderHierarchyProvider.ResolveByUniqueSuffix` (`OutlookFolderHierarchyProvider.cs:85-112`) when a persisted suggestion label (classifier corpus, subject map, or recents) names a folder that no longer exists at that path. Those rows fall back to a single segment. The change should make the fallback visibly distinct (or filter such labels) and log once per label rather than once per render. + +Runtime facts that constrain the design: `AppOlObjects.ArchiveRootPath` (`TaskMaster\AppGlobals\AppOlObjects.cs:260-270`, `AppOlObjects.ArchiveRoot.cs:86-93`) is derived from the default store root plus the literal folder `Archive`, validated by `ArchiveRootPathGuard`, and throws rather than returning null. Persisted suggestion labels are archive-relative when written correctly (`OlFolderClassifierGroup.cs:205`, `SubjectMapSco.Orchestration.cs:19-33`, `AppAutoFileObjects.cs:211-229`). + + +## Proposed Fix + +### Design summary (what changes where): + +### Boundaries and invariants to preserve: + +### Dependencies or blocked work: + +### Implementation strategy (what changes, not sequencing): + +#### Files/modules to change: + +#### Functions/classes/CLI commands impacted: + +#### Data flow and validation changes: + +#### Error handling and logging updates: + +#### Rollback/feature-flag considerations (if applicable): + +### Technical specifications (interfaces/contracts): + +#### Inputs/outputs and formats: + +#### Required configuration keys and defaults: + +#### Backward-compatibility expectations: + +#### Performance constraints (latency/throughput/memory): + +## Assumptions, Constraints, Dependencies +- Assumptions (environment, data, access): +- Constraints (budget, performance, compatibility): +- External dependencies (services, libraries, releases): + +## Data / API / Config Impact +- User-facing or API changes: +- Data or migration considerations: +- Logging/telemetry updates (if any): +- Compatibility notes (CLI flags, config schemas, versioning): + +## Test Strategy +Seeded from issue: + +Acceptance criteria settled with the maintainer on 2026-09-06: + +- [ ] AC1: Suggestion rows and search-result rows in both the QuickFiler item view and the Efc view render the lineage starting at the first segment below the archive root, with the same arrow rendering and clickable ancestor segments for both row kinds. Example: `_Active Projects -> Build RGF Org and Team -> Sales Lead`. +- [ ] AC2: A resolved chain that does not pass through the archive root node is logged as an error and rendered with the existing single-segment fallback; no row ever shows the mailbox or Archive segment. +- [ ] AC3: Filing target and score-lookup key remain the archive-relative stem (unchanged #439 constraint); filing to the selected folder still lands correctly. +- [ ] AC4: `ProjectSuggestionPath` and `ProjectPredeterminedFolder` are replaced by one shared projection built on `ArchiveStemContract.TryMakeArchiveRelative`, and the empty-root one-separator strip is eliminated. +- [ ] AC5: Recent-folder entries pass through the same projection before display. +- [ ] AC6: `EfcFormController.BindBreadcrumbRowsAsync` projects the score paths the same way as the rows, so archive-rooted suggestions retain their percentage. +- [ ] AC7: Persisted suggestion labels that fail hierarchy resolution are rendered distinguishably (or filtered) and logged once per label per session, not once per render. +- [ ] AC8: The leading-underscore rendering question (`_Active Projects` vs `_ Active Projects`) is verified and, if the renderer alters it, corrected. + +Validation: + +- [ ] Unit coverage areas: chain trimming below the archive root (chain through root, chain not through root, chain equal to root); shared projection against the #614 contract cases (case-insensitive, trailing separators, `Archive2` boundary); recents projection; Efc score join with rooted and relative paths; renderer segment text preservation for leading underscores. +- [ ] Integration scenario to retest: bind a row set with a suggestion, a search result, a `====` banner, the trash pseudo-row, and a stale label; assert lineage on both folder row kinds, fallback on the stale label, none on banner/trash. +- [ ] Manual verification notes: QuickFiler ordinary and High Confidence, plus Efc view: confirm no row begins with the mailbox or Archive, and that clicking a middle segment navigates to that ancestor. + +- Regression tests to add or update: +- Unit tests (pytest) for the fixed behavior and boundaries: +- Edge cases and negative scenarios (invalid inputs, missing data, boundary values): +- Error handling and logging verification: +- Coverage impact and targets for changed lines/modules: +- Toolchain commands to run (format → lint → type-check → test): +- Manual validation steps (if required): + + +## Acceptance Criteria +- [ ] Repro steps now produce the expected behavior in all documented environments. +- [ ] Regression test(s) added and passing (list file path and test name). +- [ ] Edge cases and invalid inputs are handled with correct errors or fallbacks. +- [ ] No unintended behavior changes outside the defined scope. +- [ ] Required logs/telemetry updated and validated (if applicable). +- [ ] Performance constraints met or explicitly waived with rationale. +- [ ] Full toolchain pass completed (format → lint → type-check → test). +- [ ] Docs/config references updated to match the new behavior. + +## Risks & Mitigations +- Technical or operational risks: +- Mitigations and rollbacks: + +## Rollout & Follow-up +- Release/rollout steps: +- Post-fix monitoring or clean-up tasks: +- Links: issue, PRs, related docs diff --git a/docs/features/potential/promoted/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency.md b/docs/features/potential/promoted/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency.md new file mode 100644 index 000000000..f0f9d0165 --- /dev/null +++ b/docs/features/potential/promoted/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency.md @@ -0,0 +1,107 @@ +# breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency (Issue #799) + +- Date captured: 2026-09-06 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency/ (Issue #799) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #799 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/799 +- Last Updated: 2026-09-07 +## Summary + +In QuickFiler (ordinary and High Confidence modes) suggestion rows render their breadcrumb lineage from the store root, for example `dmoisan@realgoodfoods.com -> Archive -> _Active Projects -> Build RGF Org and Team -> Sales Lead`, while typed search-result rows render `_Active Projects -> Build RGF Org and Team -> Sales Lead`. The mailbox and Archive segments are superfluous: every filing target is under the archive root, and the lineage must begin at the first segment below it for every row kind. The full-root lineage is the behavior delivered by issue #439 ("full root-to-leaf ancestor lineage"), so this is a specification change, not a regression. The same change should unify several archive-root stripping paths that have drifted into duplicates and gaps. + +## Environment + +- OS/version: Windows 11 Pro 10.0.26200 +- Runtime: .NET Framework 4.8 VSTO Outlook add-in with WebView2 breadcrumb, debug build from `TaskMaster\bin\Debug`, HEAD `c431dc32` (2026-09-06) +- Command/flags used: Outlook ribbon -> QuickFiler and QuickFiler High Confidence; folder drop-down in the item view (`ItemViewer` breadcrumb, `FolderBreadcrumb.html`) +- Data source or fixture: live mailbox; archive root resolves to `\\dmoisan@realgoodfoods.com\Archive` (`AppOlObjects.ArchiveRootPath`) + +## Steps to Reproduce + +1. Launch QuickFiler on Inbox. Open the folder drop-down on an item without typing. +2. Observe the first suggestion row: `dmoisan@realgoodfoods.com -> Archive -> _Active Projects -> Build RGF Org and Team -> Sales Lead`. +3. Type a few letters into the search box. Observe the same folder as a search result: `_Active Projects -> Build RGF Org and Team -> Sales Lead`. Typing `90` shows `_Active Projects -> 90 Day Plan`. +4. Accept a suggestion: filing lands in the correct Outlook and file-system folders (the filing target is the archive-relative stem, not the displayed lineage). + +Note: the maintainer's transcription of the search row showed `_ Active Projects` with a space after the underscore. The orchestrator should verify whether the renderer alters a leading underscore or whether this was a transcription artifact. + +## Expected Behavior + +- Every row in the folder list, suggestion or search result, renders as `_Active Projects -> Build RGF Org and Team -> Sales Lead`: the lineage begins at the first segment below the archive root, each segment is clickable for ancestor navigation, and no row shows the mailbox or Archive segments. +- A resolved ancestor chain that does not pass through the archive root node is logged as an error and rendered with the existing single-segment fallback, never with a mailbox prefix. There are no legitimate filing targets outside the archive root. +- One archive-root projection rule is used everywhere a suggestion path is prepared for display or compared against displayed entries. + +## Actual Behavior + +- Suggestion rows show the mailbox and Archive segments; search rows do not. Ordinary and High Confidence modes behave the same. +- Recent-folder entries are appended to the suggestion list with no projection at all. +- The Efc breadcrumb binding joins projected row text against raw scorer paths, so an archive-rooted suggestion loses its percentage. +- Several suggestion rows fail hierarchy resolution and fall back to a single segment, with errors such as `No snapshot node path ends with '\Forums\Pricing'; leaving 'Forums\Pricing' unresolved.` + +## Logs / Screenshots + +- [x] Attached minimal logs or screenshot +- Snippet (`TaskMaster\bin\Debug\logs\debug_2026-09-06.log`), stale-label resolution failures: + +``` +2026-09-06 16:45:51,908 [VSTA_Main] ERROR UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider - No snapshot node path ends with '\Scorecards\Monthly Scans'; leaving 'Scorecards\Monthly Scans' unresolved. +2026-09-06 16:45:51,937 [VSTA_Main] ERROR UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider - No snapshot node path ends with '\Forums\Pricing'; leaving 'Forums\Pricing' unresolved. +``` + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +Display-only: filing lands correctly. The redundant segments consume most of the row width and make same-named folders harder to distinguish, which is the problem #439 set out to solve. + +## Suspected Cause / Notes + +Why the two row kinds differ (verified by code read): + +- Suggestion rows: `FolderBreadcrumbBridgeRouter.SetSuggestionsAsync` decorates each row with the ancestor chain from `IFolderHierarchyProvider`. `FolderTreeSnapshotQueries.GetAncestorChain` (`UtilitiesCS\OutlookObjects\Folder\FolderTreeSnapshotQueries.cs:109-140`) walks `ParentKey` until it is null, i.e. up to the store node. `BreadcrumbRowBuilder.MapSegments` (`UtilitiesCS\OutlookObjects\Folder\BreadcrumbRowBuilder.cs:178-208`) maps every node to a segment with no trimming. This is the #439 design (`docs\features\active\2026-08-07-efcviewer-missing-lineage-and-segment-navigation-439\issue.md`, Expected Behavior). +- Search rows: `FolderBreadcrumbBridgeRouter.ReplaceItemsPreservingSession` (`FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55`) carries the verbatim archive-relative stem produced by `FolderPredictor.GetOlSubpath` (`FolderPredictor.cs:953-971`), and `BreadcrumbRenderProjection.SplitVerbatim` (`BreadcrumbRenderProjection.cs:242-246`) splits it on path separators for rendering. No provider chain, so no store segments. + +Archive-root projection paths that should be unified under `ArchiveStemContract` (`UtilitiesCS\OutlookObjects\Folder\ArchiveStemContract.cs`, the canonical ordinal, anchored, separator-terminated stripper from #614): + +- `FolderPredictor.ProjectSuggestionPath` (`FolderPredictor.cs:848-861`): private, backslash-only, appends `"\\"` to the root unconditionally, silently returns the full path on no match, and with an empty root strips one leading separator from any path. +- `QfcItemController.ProjectPredeterminedFolder` (`QuickFiler\Controllers\QfcItemController.FolderHandling.cs:272-285`): a hand-copied duplicate of the above added by #678 AC12 because the original is private. +- `FolderPredictor.AddRecents` (`FolderPredictor.cs:788-795`): appends `_globals.AF.RecentsList` verbatim with no projection. +- `FolderPredictor.GetOlSubpath` (`FolderPredictor.cs:953-971`): blind `Substring` with no prefix verification. +- `FolderMinimalWrapper.ToRelativePath` (`FolderMinimalWrapper.cs:84`) and `FolderWrapper.RelativePath` (`FolderWrapper .cs:194-224`): unanchored `Replace(root + "\\", "")` with a full-path fallback when the root is null, which is how a rooted label can enter the persisted classifier corpus. +- `EfcFormController.BindBreadcrumbRowsAsync` (`QuickFiler\Controllers\EfcFormController.cs:1115-1118`): passes projected `rows` with raw `Suggestions.ToScoredArray()` scores to `BindRowsAsync`, which joins by presented-text equality, so archive-rooted suggestions lose their score. `AddSuggestionRows` (`FolderPredictor.cs:835-846`) projects the score path; this site does not. +- `ToDoModel\Email Utilities\SortItemsToExistingFolder.cs:67-109`: legacy unanchored `Contains(StrRoot)` and `Substring` root handling. + +Stale labels: the resolution failures logged above come from `OutlookFolderHierarchyProvider.ResolveByUniqueSuffix` (`OutlookFolderHierarchyProvider.cs:85-112`) when a persisted suggestion label (classifier corpus, subject map, or recents) names a folder that no longer exists at that path. Those rows fall back to a single segment. The change should make the fallback visibly distinct (or filter such labels) and log once per label rather than once per render. + +Runtime facts that constrain the design: `AppOlObjects.ArchiveRootPath` (`TaskMaster\AppGlobals\AppOlObjects.cs:260-270`, `AppOlObjects.ArchiveRoot.cs:86-93`) is derived from the default store root plus the literal folder `Archive`, validated by `ArchiveRootPathGuard`, and throws rather than returning null. Persisted suggestion labels are archive-relative when written correctly (`OlFolderClassifierGroup.cs:205`, `SubjectMapSco.Orchestration.cs:19-33`, `AppAutoFileObjects.cs:211-229`). + +## Proposed Fix / Validation Ideas + +Acceptance criteria settled with the maintainer on 2026-09-06: + +- [ ] AC1: Suggestion rows and search-result rows in both the QuickFiler item view and the Efc view render the lineage starting at the first segment below the archive root, with the same arrow rendering and clickable ancestor segments for both row kinds. Example: `_Active Projects -> Build RGF Org and Team -> Sales Lead`. +- [ ] AC2: A resolved chain that does not pass through the archive root node is logged as an error and rendered with the existing single-segment fallback; no row ever shows the mailbox or Archive segment. +- [ ] AC3: Filing target and score-lookup key remain the archive-relative stem (unchanged #439 constraint); filing to the selected folder still lands correctly. +- [ ] AC4: `ProjectSuggestionPath` and `ProjectPredeterminedFolder` are replaced by one shared projection built on `ArchiveStemContract.TryMakeArchiveRelative`, and the empty-root one-separator strip is eliminated. +- [ ] AC5: Recent-folder entries pass through the same projection before display. +- [ ] AC6: `EfcFormController.BindBreadcrumbRowsAsync` projects the score paths the same way as the rows, so archive-rooted suggestions retain their percentage. +- [ ] AC7: Persisted suggestion labels that fail hierarchy resolution are rendered distinguishably (or filtered) and logged once per label per session, not once per render. +- [ ] AC8: The leading-underscore rendering question (`_Active Projects` vs `_ Active Projects`) is verified and, if the renderer alters it, corrected. + +Validation: + +- [ ] Unit coverage areas: chain trimming below the archive root (chain through root, chain not through root, chain equal to root); shared projection against the #614 contract cases (case-insensitive, trailing separators, `Archive2` boundary); recents projection; Efc score join with rooted and relative paths; renderer segment text preservation for leading underscores. +- [ ] Integration scenario to retest: bind a row set with a suggestion, a search result, a `====` banner, the trash pseudo-row, and a stale label; assert lineage on both folder row kinds, fallback on the stale label, none on banner/trash. +- [ ] Manual verification notes: QuickFiler ordinary and High Confidence, plus Efc view: confirm no row begins with the mailbox or Archive, and that clicking a middle segment navigates to that ancestor. + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch From 6be47923b87c77d300c8b3f5127399628f01fcda Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sun, 6 Sep 2026 22:32:06 -0400 Subject: [PATCH 02/13] docs(799): add research findings and author the bug spec Records the verified research artifact covering the seven archive-root stripping sites, the chain-trim seam, the Efc score join, the stale-label log gate, and the leading-underscore question. Authors spec.md with the eight maintainer-settled acceptance criteria verbatim, an explicit statement that this supersedes the issue #439 root-to-leaf lineage rather than fixing a regression, and a Write Set section. Findings of note: the leading-underscore report is a transcription artifact and needs no code change; the ToDoModel sort utility is not a compile item and has no live caller; the recents row-model mirror is a second unprojected site the issue did not report. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MS8fb2wX1Sx1bG67LAr41e --- ...rchive-root-projection.2026-09-06T18-20.md | 1089 +++++++++++++++++ .../spec.md | 884 +++++++++++-- 2 files changed, 1896 insertions(+), 77 deletions(-) create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/research/archive-root-projection.2026-09-06T18-20.md diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/research/archive-root-projection.2026-09-06T18-20.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/research/archive-root-projection.2026-09-06T18-20.md new file mode 100644 index 000000000..dcf78fd11 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/research/archive-root-projection.2026-09-06T18-20.md @@ -0,0 +1,1089 @@ +# Archive-root projection and breadcrumb lineage (Issue #799) + +- Date: 2026-09-06T18-20 +- Worktree: `/.claude/worktrees/` on `bug/breadcrumb-lineage-below-archive-root-799` (branched from `origin/main` at `c431dc32`) +- Scope: research only. No source, config, or project file was modified. +- Every file path below is repository-relative and was read in THIS worktree. Line numbers are + from the files as they exist here, not copied from `issue.md`. + +--- + +## R1. `ArchiveStemContract` + +**Location confirmed**: `UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs` (147 lines, `#nullable enable`, +`public static class`, namespace `UtilitiesCS.OutlookObjects.Folder`). + +**Compile entry**: `UtilitiesCS/UtilitiesCS.csproj:623` — `` + +### Full public surface (three members, no others) + +```csharp +public static bool IsFullOutlookPath(string value) // :41 +public static void RequireArchiveRelativeStem(string value, string paramName) // :68 +public static bool TryMakeArchiveRelative(string fullPath, string archiveRoot, out string stem) // :106 +``` + +Private constants only: `BackslashSeparator = '\\'` (:20), `ForwardSeparator = '/'` (:21). + +### `TryMakeArchiveRelative` exact semantics (read from :106-145) + +| Aspect | Verified behavior | Evidence | +|---|---|---| +| Anchoring | Prefix-anchored via `StartsWith`; never a `Contains` or `Replace` | `:131` `!fullPath.StartsWith(root, StringComparison.OrdinalIgnoreCase)` | +| Comparison | Ordinal, case-INsensitive on both the equality and the prefix test | `:124`, `:131` both `StringComparison.OrdinalIgnoreCase` | +| Separator termination | The character at `root.Length` must be `\` or `/`, else `false` | `:137-141` | +| Trailing separators on root | Trimmed before use (`\` and `/`) | `:118` `archiveRoot.TrimEnd(BackslashSeparator, ForwardSeparator)` | +| Null / empty / whitespace `archiveRoot` | Returns `false`, `stem = string.Empty`. Also `false` when the root is nothing but separators (`root.Length == 0` after trim) | `:113-122` | +| Null / empty `fullPath` | Returns `false`, `stem = string.Empty` | `:113` | +| Path EQUALS the root | Returns **`true`** with `stem = string.Empty` | `:124-127` | +| Path not under root | Returns `false` and yields `string.Empty` — it never passes the input through | `:112`, `:129-141` | +| `Archive2` false prefix | `\\mbx\Archive2\X` against root `\\mbx\Archive` -> `fullPath[root.Length] == '2'`, not a separator -> `false` | `:137-141` | +| Leading separator on the stem | Stripped (`TrimStart` of both separator chars), so the stem never leads with `\` | `:143` | +| Purity | No I/O, no COM, no logging, no per-call regex allocation (documented at `:13-16`) | class doc | + +### Which of the seven drifted sites the contract can serve as-is + +| Site | Servable as-is? | Note | +|---|---|---| +| (a) `FolderPredictor.ProjectSuggestionPath` | Yes | Needs a thin wrapper that returns the input unchanged on `false`, to preserve the current display behavior for out-of-root paths | +| (b) `QfcItemController.ProjectPredeterminedFolder` | Yes | Same wrapper | +| (c) `FolderPredictor.AddRecents` / `AddRecentRows` | Yes | Same wrapper applied per recent entry | +| (d) `FolderPredictor.GetOlSubpath` | **Partially** | Only the `includeChildren == true` branch is a stem strip. The `false` branch returns the LEAF NAME, which the contract does not compute | +| (e) `FolderMinimalWrapper.ToRelativePath` | Yes technically, but see R2(e) — a caller depends on the divergence | +| (f) `FolderWrapper.LoadRelativePath` | Same as (e) | +| (g) `SortItemsToExistingFolder` | N/A — the file is not compiled (R2(g)) | + +### Is a NEW contract method needed? + +**Yes, one — but as a separate small type, not as a new member on `ArchiveStemContract`.** + +`ArchiveStemContract` is deliberately a hard boundary type: on failure it yields empty and never +passes the input through (`:95-97`). Every display site (a), (b), (c) needs the OPPOSITE fallback +(return the input unchanged so an unexpected value still renders). Bolting a +"lenient" overload onto the strict contract would blur the very invariant #614 created it for. + +Recommended new pure helper, in a NEW file so `FolderPredictor.cs` (1003 lines, already double the +500-line ceiling) does not grow: + +```csharp +// UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs +public static class ArchiveStemProjection +{ + /// Display projection: the archive-relative stem when the path is strictly UNDER the + /// root; otherwise the input unchanged. An empty/whitespace root yields the input unchanged + /// (this is what eliminates the AC4 one-separator strip). + public static string ToDisplayStem(string folderPath, string archiveRoot); +} +``` + +`ToDisplayStem` must treat "path equals root" as **not** projectable (`TryMakeArchiveRelative` +returns `true` with an empty stem there, and an empty display row is worse than the full path), +so the implementation is `TryMakeArchiveRelative(...) && stem.Length > 0 ? stem : folderPath`. +That reproduces the current `folderPath.Length > archivePrefix.Length` guard at +`FolderPredictor.cs:858` exactly. + +A second new pure helper is needed for AC1/AC2 chain trimming — see R3. + +--- + +## R2. The seven drifted archive-root stripping sites + +### (a) `FolderPredictor.ProjectSuggestionPath` — CONVERT + +`UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:848-861`. Issue citation (848-861) is CORRECT. + +```csharp +private string ProjectSuggestionPath(string folderPath) +{ + if (_globals is null) + { + return folderPath; + } + + var archivePrefix = _globals.Ol.ArchiveRootPath + "\\"; + return + folderPath.StartsWith(archivePrefix, StringComparison.OrdinalIgnoreCase) + && folderPath.Length > archivePrefix.Length + ? folderPath.Substring(archivePrefix.Length) + : folderPath; +} +``` + +Divergences from `ArchiveStemContract.TryMakeArchiveRelative`: +1. Backslash only. A `/`-separated root or path is not matched. +2. Appends `"\\"` unconditionally, so a root already ending in `\` produces the prefix `...\\` and + never matches. +3. With an EMPTY root the prefix becomes `"\"`, so ANY path leading with a separator has exactly one + leading separator stripped — the AC4 defect. +4. Correct anchoring and ordinal-ignore-case otherwise; the `Archive2` boundary is handled by + accident because the prefix carries the trailing `\`. + +**Callers**: `FolderPredictor.cs:810` (`AddSuggestions`) and `FolderPredictor.cs:842` +(`AddSuggestionRows`). Both are display paths. CONVERT to `ArchiveStemProjection.ToDisplayStem`. + +### (b) `QfcItemController.ProjectPredeterminedFolder` — CONVERT + +`QuickFiler/Controllers/QfcItemController.FolderHandling.cs:272-285`. Issue citation (272-285) is CORRECT. + +```csharp +internal static string ProjectPredeterminedFolder(string folderPath, string archiveRootPath) +{ + if (string.IsNullOrEmpty(folderPath) || archiveRootPath is null) + { + return folderPath; + } + + string archivePrefix = archiveRootPath + "\\"; + return + folderPath.StartsWith(archivePrefix, StringComparison.OrdinalIgnoreCase) + && folderPath.Length > archivePrefix.Length + ? folderPath.Substring(archivePrefix.Length) + : folderPath; +} +``` + +Identical stripping expression to (a); the only differences are the null guards documented in the +XML comment at `:252-271`. Sole caller: `QuickFiler/Controllers/QfcItemController.FolderHandling.cs:231-234` +inside `AssignFolderComboBox`. Its own comment at `:228-230` states the duplication exists only +because `ProjectSuggestionPath` is private — that reason disappears once the shared helper is +public. CONVERT; keep the member as a one-line delegation so +`QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs:213-241` keeps a target. + +Note the existing test at `:220-223` pins the EMPTY-root one-separator strip. AC4 explicitly +eliminates that behavior, so that assertion must be updated as part of the change, not preserved. + +### (c) `FolderPredictor.AddRecents` — CONVERT + +`UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:788-795`. Issue citation is CORRECT. + +```csharp +public void AddRecents(ref List folderList) // internal +{ + if (_globals.AF.RecentsList.Count > 0) + { + folderList.Add("======= RECENT SELECTIONS ========"); + folderList.AddRange(_globals.AF.RecentsList); + } +} +``` + +No projection at all. **The issue under-reports this site**: the row-model mirror +`AddRecentRows` at `FolderPredictor.cs:866-882` has the same gap +(`rows.Add(new FolderRow(recent, FolderRowKind.Recent, null))` at `:879`) and is the one the +breadcrumb surfaces actually consume. AC5 requires BOTH be projected, otherwise the string list and +the row list disagree — and `FolderRowArray`'s XML doc at `:233-242` asserts they are text-identical. +CONVERT both. + +### (d) `FolderPredictor.GetOlSubpath` — CONVERT the `includeChildren == true` branch only + +`UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:953-971`. Issue citation is CORRECT. + +```csharp +public string GetOlSubpath(string path, string olAncestor, bool includeChildren) +{ + if (includeChildren) + { + if (olAncestor.EndsWith('\\'.ToString())) + { + return path.Substring(olAncestor.Length); + } + else + { + return path.Substring(olAncestor.Length + 1); + } + } + else + { + var pathParts = path.Substring(olAncestor.Length).Split(@"\"); + return pathParts[pathParts.Count() - 1]; + } +} +``` + +Divergences: no prefix verification at all. `path` is assumed to start with `olAncestor`. If it does +not, the result is a garbage substring; if `path.Length <= olAncestor.Length` it throws +`ArgumentOutOfRangeException`. Case is irrelevant because no comparison happens. `Archive2` is +mis-stripped silently. + +`olAncestor` here is a SEARCH root, not necessarily the archive root — `LoopFolders` at `:911-913` +falls back to `_globals.Ol.ArchiveRootPath` but `FindFolder` at `:314-326` passes each entry of the +caller-supplied `emailSearchRoots`. `TryMakeArchiveRelative` is root-agnostic (its parameter is only +NAMED `archiveRoot`), so it serves this site. + +**Callers**: `FolderPredictor.cs:918` and `:935` (both inside `LoopFolders`, which only ever passes +strict descendants), plus the public test caller +`ToDoModel.Test/Email Utilities/FolderHandlerTests_Written.cs:46,63`. Those two tests pin +`("\\\\email@company.com\\Folder 1\\Folder 2\\Folder 3", "\\\\email@company.com", true) -> "Folder 1\\Folder 2\\Folder 3"` +and the `false` variant `-> "Folder 3"`; both survive a contract-based rewrite unchanged. +CONVERT the `true` branch; leave the `false` branch's leaf-name computation alone (it is a different +function and the contract does not compute it). + +### (e) `FolderMinimalWrapper.ToRelativePath` — LEAVE ALONE + +`UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs:56-86` (issue cites `:84`, which is the +`Replace` line specifically — correct, but the member spans 56-86). + +```csharp +return OlFolder.FolderPath.Replace(OlRoot.FolderPath + "\\", ""); // :84 +``` + +Divergences: unanchored `Replace` (removes EVERY occurrence anywhere in the path, not just a +prefix); case-SENSITIVE (`string.Replace(string,string)` is ordinal case-sensitive on .NET +Framework); no `Archive2` protection beyond the appended `\`; and on `OlRoot is null` (:58-65) or a +non-containing path (:74-81) it returns the FULL path, only logging a warning. + +**Reason to leave alone**: a caller depends on the full-path fallback. +`FolderMinimalWrapper.RestoreFromRelativePath` at `:88-131` has an explicit branch +`if (RelativePath.StartsWith("\\\\"))` (`:105-114`) that walks the COM parent chain to recover a +folder whose stored `RelativePath` is a FULL store path. That branch exists only because +`ToRelativePath` can return a rooted value. Converting to the contract (which yields empty on +failure) would silently break that restore path, and `RelativePath` is `[JsonProperty]`-persisted, +so the change touches serialized data. No AC1-AC8 covers it. + +### (f) `FolderWrapper.LoadRelativePath` — LEAVE ALONE + +**On-disk filename, character for character**: `UtilitiesCS/OutlookObjects/Folder/FolderWrapper .cs` +— `FolderWrapper`, one U+0020 SPACE, then `.cs`. Confirmed by `Glob` output and by the csproj entry +at `UtilitiesCS/UtilitiesCS.csproj:824`. + +The issue names the member `FolderWrapper.RelativePath` at `:194-224`. **The citation's line span is +correct but the member name is wrong**: `RelativePath` is the `[JsonProperty]` accessor at +`:187-192`; the stripping lives in `internal virtual string? LoadRelativePath()` at `:194-224`. The +body is character-identical to (e), including `return OlFolder.FolderPath.Replace(OlRoot.FolderPath + "\\", "");` +at `:222`. + +**Reasons to leave alone**: +1. The file is **532 lines** — already over the repository's 500-line ceiling. Any edit invites a + split that is out of scope here. +2. The full-path fallback feeds the persisted classifier corpus through + `UtilitiesCS/EmailIntelligence/EmailParsingSorting/SortEmail.cs:597` + (`.UnTrain(helper.FolderInfo!.RelativePath, ...)`) and + `QuickFiler/Controllers/QfcFormController.Actions.cs:250`. Changing the value shape changes + training data. That is a genuine defect but a DIFFERENT one, with no AC in #799. + +Recommend promoting (e)+(f) as a follow-up issue rather than folding them into #799. + +### (g) `SortItemsToExistingFolder` — LEAVE ALONE (not compiled) + +**On-disk directory name, character for character**: `ToDoModel/Email Utilities/` — `Email`, one +U+0020 SPACE, `Utilities`. File: `ToDoModel/Email Utilities/SortItemsToExistingFolder.cs` (403 lines). + +The issue cites `:67-109`. Verified content at those lines: + +```csharp +if (string.IsNullOrEmpty(StrRoot)) { StrRoot = _globals.Ol.ArchiveRootPath; } // :65-68 +loc = StrRoot + @"\"; // :70 +... +else if (folderCurrent.FolderPath.Contains(StrRoot) & (folderCurrent.FolderPath != StrRoot)) // :90-92 +... +strTemp2 = _globals.Ol.ArchiveRootPath.Substring(_globals.Ol.EmailRootPath.Length); // :109 +``` + +Divergences: `Contains` (unanchored, case-sensitive), bitwise `&` instead of `&&`, and a blind +`Substring` against a DIFFERENT root (`EmailRootPath`, not the archive root). + +**Decisive reason to leave alone: the file is not compiled.** +`ToDoModel/ToDoModel.csproj` is a non-SDK-style project (`` +at `:2`) with explicit `` items and no globbing. Its complete `Email Utilities` +membership is one line: + +```xml + +``` + +`SortItemsToExistingFolder.cs` (and `CaptureEmailDetailsModule.cs`) appear only in the stale +`ToDoModel/ToDoModel.csproj.bak:138`. + +**Searches run**: (1) `Grep` for `SortItemsToExistingFolder\.cs` restricted to `*.csproj` -> "No matches found"; +(2) `Grep` for `MASTER_SortEmailsToExistingFolder|SortItemsToExistingFolder` repo-wide -> the only +non-doc hits are the source file itself, `ToDoModel.csproj.bak:138`, and test files that reference +the class name only inside comments (`ToDoModel.Test/Email Utilities/SortItemsToExistingFolderTests.cs:62` +is a commented-out call). There is no live caller of `MASTER_SortEmailsToExistingFolder` anywhere. + +Adding the file back to the build to fix it would introduce a new compile unit with ~7 unrelated +compiler problems (unused locals, `null` defaults on value contexts) and is unquestionably out of +scope. + +### Additional site found by the cross-check search, NOT named in the issue + +`UtilitiesCS/OutlookObjects/MailItem/MailItemHelper.Loading.cs:122-130`: + +```csharp +internal static Folder ResolveFolderRoot(IApplicationGlobals appGlobals, string folderPath) +{ + if (folderPath.Contains(appGlobals.Ol.ArchiveRootPath)) { return appGlobals.Ol.ArchiveRoot; } + return appGlobals.Ol.Inbox; +} +``` + +This is an unanchored archive-root COMPARISON, not a stripper — it selects which `OlRoot` is handed +to the wrappers in (e)/(f). Recorded for completeness; out of scope for #799. + +--- + +## R3. Chain trimming below the archive root (AC1, AC2) + +### What the chain is and where it starts + +`FolderTreeSnapshotQueries.GetAncestorChain` — `UtilitiesCS/OutlookObjects/Folder/FolderTreeSnapshotQueries.cs:109-146`. +The issue cites `:109-140`; the member actually runs to `:146`. Behavior: + +```csharp +while (current != null && visited.Add(current.Key)) +{ + chain.Add(current); + if (current.ParentKey == null || !snapshot.TryGetNode(current.ParentKey, out var parent)) { break; } + current = parent; +} +chain.Reverse(); +``` + +**The snapshot DOES contain the store/mailbox node.** `OutlookFolderHierarchyReader.ReadStoreAsync` +(`UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyReader.cs:117-160`) pushes the store root +folder itself (`stack.Push(Tuple.Create(root, string.Empty, root.FolderPath))` at `:127`) with an +empty `parentEntryId`, and `ToNode` (`:177-204`) resolves `parentKey` to `null` for it. So +`ParentKey == null` identifies exactly the store root, and the walk terminates there. This is why the +rendered lineage begins ` -> Archive -> ...`. + +`BreadcrumbRowBuilder.MapSegments` — `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:178-208` +(issue cites `:178-208`, CORRECT). It is a pure 1:1 map with no trimming and no archive-root +knowledge. + +### What identifies the archive-root node + +Not an EntryID, not `ParentKey == null`, and not a literal child named "Archive". The reliable +identifier is **`FolderTreeSnapshotNode.FolderPath` compared ordinal-case-insensitively against +`IOlObjects.ArchiveRootPath`, with trailing separators trimmed** — i.e. exactly +`ArchiveStemContract.TryMakeArchiveRelative`'s root handling. + +Evidence that the two strings are the same shape: +- `AppOlObjects.ResolveValidatedArchiveRootPath()` (`TaskMaster/AppGlobals/AppOlObjects.ArchiveRoot.cs:86-93`) + computes `Path.Combine(Root.FolderPath, "Archive")` and requires it to equal + `ArchiveRoot.FolderPath` via `ArchiveRootPathGuard.RequireResolvedArchiveRoot`. It throws rather + than returning null (`:60-64`, and `TaskMaster/AppGlobals/AppOlObjects.cs:260-269` caches it). +- `OutlookFolderHierarchyProvider.ResolveLeafKeyAsync` (`:70-72`) already matches presented paths + against `node.FolderPath`, and `BreadcrumbBridgeRouter.ToHierarchyPath` + (`QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:152-167`) builds `_boundRoot + "\\" + stem` for + that same comparison. The two are interoperable today. + +`FolderTreeSnapshotNode.RelativePath` is NOT usable: `OutlookFolderHierarchyReader.GetRelativePath` +(`:206-211`) makes it relative to the STORE root and uses the same unanchored +`Replace(rootPath + "\\", "")` pattern as (e)/(f). + +### Where to apply the trim — smallest diff AND best sibling isolation + +**Recommendation: `OutlookFolderHierarchyProvider.GetAncestorChainAsync` +(`UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs:34-42`, file is 141 lines).** + +```csharp +public async Task> GetAncestorChainAsync( + FolderTreeNodeKey leafKey, CancellationToken cancellationToken) +{ + var snapshot = await AcquireSnapshotAsync(cancellationToken).ConfigureAwait(false); + var chain = FolderTreeSnapshotQueries.GetAncestorChain(snapshot, leafKey); + return MapNodes(chain); +} +``` + +Rationale, in order of weight: + +1. **It is the only point both surfaces share.** The QuickFiler drop-down reaches the chain through + `FolderBreadcrumbBridgeRouter.SetSuggestionsAsync` (`UtilitiesCS/.../FolderBreadcrumbBridgeRouter.cs:52-60`) + and the Efc list through `BreadcrumbBridgeRouter.FetchChainAsync` + (`QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs:50-80`). Both call + `_provider.GetAncestorChainAsync`. One change satisfies AC1 on both. +2. **It keeps this item's diff entirely off the sibling's files** — see R9. Trimming inside + `FolderBreadcrumbBridgeRouter.SetSuggestionsAsync` would require a new field and a constructor + parameter on the exact type the sibling owns. +3. `GetImmediateSubfoldersAsync` (`:45-53`) is unaffected: subfolders are always below the leaf, so + below the archive root by construction. +4. The AC7 log site (`ResolveByUniqueSuffix`, `:90-114`) is in the same 141-line file, so AC2 and + AC7 land together. + +**Data the trim needs that is not available at that call site**: only the archive root string. The +provider is constructed with `IOutlookFolderTreeService` alone (`:28-31`). Supply it as an OPTIONAL +second constructor parameter of type `Func` (a lazy accessor, NOT an eagerly-read string): + +- Lazy is mandatory. `IOlObjects.ArchiveRootPath` THROWS `InvalidOperationException` when the root is + unresolvable (`AppOlObjects.ArchiveRoot.cs:35-39`), and + `QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242` + (`BindBreadcrumbRowsAsync_WhenArchiveRootThrows_ReportsOnceAndDoesNotThrow`) exists specifically + because of that. Reading it eagerly at construction creates a NEW throw site inside + `EfcFormController.ConfigureBreadcrumbControl` and `QfcItemController.EnsureBreadcrumbPipeline`, + neither of which is inside a try. +- Optional (defaulting to `null` = no trim) keeps all 20 existing test constructions in + `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` and + `UtilitiesCS.Test/OutlookObjects/Folder/FolderHierarchyProviderAdapterTests.cs` compiling + unchanged, including `new OutlookFolderHierarchyProvider(null)` at + `OutlookFolderHierarchyProviderTests.cs:316` (a single `null` argument binds unambiguously to the + first parameter). + +Put the trim itself in a NEW pure static so it is unit-testable without a provider or a snapshot: + +```csharp +// UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs +public static bool TryTrimBelowArchiveRoot( + IReadOnlyList chain, + string archiveRoot, + out IReadOnlyList trimmed); +``` + +Implementation: find the first index `i` where +`ArchiveStemContract.TryMakeArchiveRelative(chain[i].FolderPath, archiveRoot, out var s) && s.Length == 0` +(that is the root node itself, since `TryMakeArchiveRelative` returns `true` with an empty stem on +exact equality, `:124-127`). Return `chain[(i+1)..]`. Return `false` when no such index exists, or +when `i` is the last index (the leaf IS the root, so there is nothing to render below it). + +### Detecting a chain that does NOT pass through the archive root, and the fallback + +Detection is exactly the `false` return above: no chain element's `FolderPath` equals the archive +root. `OutlookFolderHierarchyProvider` then emits `logger.Error(...)` — the same `log4net.ILog` +already declared at `:17-19` — and returns `Array.Empty()`. + +**The existing single-segment fallback code path, named exactly:** + +- **Efc surface**: `BreadcrumbRowBuilder.BuildRow`, `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:122-131`: + ```csharp + IReadOnlyList segments = MapSegments(ancestorChain); + if (segments.Count == 0) + { + // Unknown/empty chain fallback: render the presented path as a single + // leaf-only segment so the suggestion stays visible and selectable. + segments = new[] { new BreadcrumbSegment(presentedText, LeafToken(presentedText), false) }; + } + ``` + An EMPTY (non-null) chain reaches this branch: `BreadcrumbBridgeRouter.BindRowsAsync:126-129` + stores any non-null chain in `chains`, and `MapSegments` returns empty for an empty input + (`BreadcrumbRowBuilder.cs:182-185`). `LeafToken` (`:231-236`) yields the last path segment, so the + row renders as a genuine SINGLE segment. Confirmed correct. + +- **QuickFiler surface**: `FolderBreadcrumbBridgeRouter.CreateFallbackRow`, + `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs:247-257`, selected by the + `chain != null && chain.Count > 0` ternary at `:61-71`. It produces the scored-fallback row + (`new BreadcrumbStateRow(identity, row.Score.Value.FolderPath, row.Score.Value.Probability)`, + which sets `IsScoredFallback = true` via `BreadcrumbStateModel.Row.cs:112-113`). + + **Caveat the planner must know**: on this surface the fallback is NOT single-segment. + `BreadcrumbRenderProjection.ProjectRow` (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbRenderProjection.cs:176-178`) + routes non-`IsSuggestion` rows through `SplitVerbatim` (`:242-246`), which splits on `\`. A + fallback for `_Active Projects\Build RGF Org and Team\Sales Lead` therefore renders as three + segments, not one. This is the EXISTING behavior and it already satisfies AC2's real requirement + ("never with a mailbox prefix"), because the verbatim text is the archive-relative stem. AC2's + phrase "existing single-segment fallback" is literally true only on the Efc surface. Do not + "fix" the QFC surface to one segment — that would be a new regression against the search-row + rendering that AC1 says must match. + +--- + +## R4. AC6 — the Efc score join + +### Read chain + +- `EfcFormController.BindBreadcrumbRowsAsync` — `QuickFiler/Controllers/EfcFormController.cs:1111-1128`. + Issue cites `:1115-1118`; the member spans 1111-1128, and the two load-bearing lines are + `:1115-1117` and `:1118`. Correct region. + ```csharp + var scores = _dataModel?.FolderHelper?.Suggestions?.ToScoredArray() ?? Array.Empty(); + await _router.BindRowsAsync(rows, scores, _globals.Ol.ArchiveRootPath, Token); + ``` +- `BreadcrumbBridgeRouter.BindRowsAsync` (internal 4-arg) — `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:92-150`. +- `BreadcrumbRowBuilder.BuildRows` -> `BuildProbabilityIndex` -> `BuildRow` — + `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:35-60`, `:210-229`, `:133-135`. +- `FolderPredictor.AddSuggestionRows` — `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:835-846`. + Issue cites `:835-846`. CORRECT. + +### How the percentage is lost + +`AddSuggestionRows` projects BOTH the row text and the score: + +```csharp +var folderPath = ProjectSuggestionPath(score.FolderPath); // :842 +var projectedScore = new FolderScore(folderPath, score.Score, score.Probability); // :843 +rows.Add(new FolderRow(folderPath, FolderRowKind.Suggestion, projectedScore)); // :844 +``` + +`BindBreadcrumbRowsAsync` does not use that row model. It takes `rows` (a `string[]` that came from +`FolderPredictor.FolderArray`, i.e. already projected by `ProjectSuggestionPath` at `:810`) and +pairs them with `Suggestions.ToScoredArray()` — the **raw, unprojected** scorer output. + +`BreadcrumbRowBuilder.BuildProbabilityIndex` keys the dictionary on `score.FolderPath` (`:222-224`), +and `BuildRow` looks up `probabilityByPath.TryGetValue(presentedText, out double p)` (`:133`). The +comparer is `StringComparer.OrdinalIgnoreCase` (`:219`), so case is not the problem — the KEY is. +For an archive-rooted suggestion the key is `\\\Archive\Forums\Pricing` while +`presentedText` is `Forums\Pricing`. Lookup misses, `probability` is `null`, and +`PercentageFormatter.FormatPercent(null)` renders an empty `.pct` cell +(`UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs:205-211`). + +### Minimal correct fix, and which is smaller/safer + +Two candidates: + +**(A) Project the score paths at `EfcFormController.cs:1115-1117`.** One statement, but +`EfcFormController.cs` is **1320 lines** — already 2.6x the 500-line ceiling. Every line added there +worsens a standing violation. + +**(B) Project the score paths inside `BreadcrumbBridgeRouter.BindRowsAsync`, using `_boundRoot`.** +RECOMMENDED. `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` is **304 lines**, has room, and +already computes and normalizes the root at `:107-109`: +```csharp +_boundRoot = string.IsNullOrWhiteSpace(archiveRootPath) ? string.Empty : archiveRootPath.TrimEnd('\\', '/'); +``` +and already consumes `ArchiveStemContract` at `:157` and `:164`. Insert, immediately before the +`_builder.BuildRows(...)` call at `:132`, a projection of each `FolderScore.FolderPath` through +`ArchiveStemProjection.ToDisplayStem(path, _boundRoot)`. + +Why (B) is safer as well as smaller: +- The public 3-arg overload (`:75-82`) forwards `string.Empty`, so `_boundRoot` is empty and + `ToDisplayStem` is the identity. No existing caller of the public overload changes behavior; that + covers every `BindRowsAsync(rows, scores, token)` call in `QuickFiler.Test`. +- It fixes the join for ALL callers of the internal overload, not just the one in + `EfcFormController`. +- It leaves `EfcFormController.cs` untouched, which matters because that file cannot absorb growth. + +Keying the join on something other than presented text was considered and rejected: `BuildRows` +takes only `IReadOnlyList` plus `IEnumerable` (`:35-39`), so there is no +correlating identity to key on without changing that public signature and every test that calls it +(`UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowBuilderTests.cs` plus 12 router test files). + +--- + +## R5. AC7 — stale-label logging and distinguishable rendering + +### The log site + +`OutlookFolderHierarchyProvider.ResolveByUniqueSuffix` — +`UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs:90-114`. The issue cites +`:85-112`; the correct span is `:90-114`, with the emission at `:108-112`: + +```csharp +logger.Error( + candidates.Length == 0 + ? $"No snapshot node path ends with '{suffix}'; leaving '{folderPath}' unresolved." + : $"Multiple snapshot node paths end with '{suffix}'; leaving '{folderPath}' unresolved." +); +return null; +``` + +### Runtime evidence (redacted) + +`/logs/debug_2026-09-06.log` contains **18** occurrences of that error family across +one session, but only **two distinct labels**: `Scorecards\Monthly Scans` and `Forums\Pricing`. +Confirms once-per-render, not once-per-label. All 18 are on the `VSTA_Main` thread. + +The log also shows a second, richer failure the issue did not report: some entries carry a suffix of +`'\\\\Archive\Forums\Pricing'` — i.e. a FULL rooted path reached +`ResolveByUniqueSuffix`. That is the Efc route: `ToHierarchyPath` +(`QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:152-167`) re-roots a relative presented target to +`_boundRoot + "\\" + target`, `ResolveLeafKeyAsync`'s exact-path match fails, and the suffix pass is +then handed a rooted string. Both surfaces log; the gate must cover both. + +### Where a once-per-label-per-session gate can live + +Inside `OutlookFolderHierarchyProvider` as a private instance field, next to the existing +`_treeService` (`:21`). The existing logging pattern in this file and assembly is: + +```csharp +private static readonly log4net.ILog logger = log4net.LogManager.GetLogger( + System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); // :17-19 +``` +(the same shape as `FolderWrapper .cs`, `FolderMinimalWrapper.cs`, and +`QuickFiler/Controllers/BreadcrumbBridgeRouter.cs:21-23`, which names its field `log`). + +**State required**: a set of already-reported labels. `ResolveByUniqueSuffix` is currently `private +static`, so the gate forces it to become an instance member (or the set becomes static). + +**Concurrency and lifetime — this matters because the item is scheduled against siblings:** +- The provider is a per-surface instance (`EfcFormController.cs:1053`, + `QfcItemController.ViewerSetup.cs:147`), created once per form/viewer. An INSTANCE set therefore + scopes to the surface, not the session. "Per session" in AC7 is satisfied more literally by a + `static` set — but a static set is process-wide mutable state shared across every QuickFiler + viewer instance and across test methods in the same assembly, which breaks the repository's test + independence rule (`.claude/rules/general-unit-test.md`, "Tests must not rely on mutable global + state"). +- Recommendation: **instance-scoped `HashSet` guarded by a `lock`, injected-clock-free**, and + spell out in the spec that "per session" is realized as "per provider instance". Calls are + observed on `VSTA_Main` in the log, but `GetAncestorChainAsync`/`ResolveLeafKeyAsync` are `async` + and `AcquireSnapshotAsync` awaits, so continuations are not guaranteed to be on one thread — + `ConcurrentDictionary` with `TryAdd`, or a `lock`ed `HashSet`, is required. A bare + `HashSet` is not safe here. +- Lifetime: the provider is referenced by the router, which the coordinator/controller disposes with + the viewer. No leak beyond viewer lifetime. Note that QuickFiler viewers are POOLED + (`Reset()`/`Clear()` on `BreadcrumbBridgeCoordinator.cs:100-110`), but the provider is recreated + only when `viewer.BreadcrumbCoordinator == null` (`QfcItemController.ViewerSetup.cs:145-151`), so a + pooled viewer keeps its provider and its gate across items — which is the desired behavior. + +### "Rendered distinguishably" — is there an existing flag or CSS class? + +**No. A new field on the row DTO and its JSON contract IS required.** Stated plainly. + +Verified absences: +- Efc row model `BreadcrumbRow` (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs:34-80`) carries + `RowId`, `Kind`, `Segments`, `Probability`, `FilingTarget`, `ActiveSegmentIndex` — no + unresolved/fallback flag. +- Efc renderer emits a fixed class string for every suggestion: + `sb.Append("
...")` + (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs:142`). `BreadcrumbDocumentAssets.BaseCss` + (`:17-29`) defines `.rows .row .crumb .pct .rowwrap .row.selectable .seg .sep .affordance + .row.banner .children .child` — no stale/unresolved class. +- QFC render DTO `BreadcrumbRowRender` (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbRenderProjection.cs:74-120`) + exposes `RowIndex, IsSuggestion, Selected, Collapsed, LeafExpanded, PercentText, Cells, Subfolders` + — no flag. `RowToJson` (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbBridgeMessages.cs:315-336`) + and `ParseRows` (`:350-367`) mirror exactly those eight fields. +- QFC page CSS (`QuickFiler/Resources/FolderBreadcrumb.html:100-158`) defines + `.row .row.selected .row.active .crumbs .seg .seg.trunc .arrow .aff .pct .subs .sub .sub.selected` + — no stale class. + +One piece of luck: the QFC state model ALREADY carries the signal. +`BreadcrumbStateRow.IsScoredFallback` (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbStateModel.Row.cs:139`) +is `true` exactly for an unresolved scored suggestion, and `BreadcrumbRenderProjection.ProjectRow` +already reads it at `:230` and `:234`. So the QFC side needs no router change to SOURCE the flag — +only to PUBLISH it. + +**Every file that would have to change for "rendered distinguishably" on both surfaces:** + +QuickFiler drop-down surface: +1. `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRenderProjection.cs` — add `IsUnresolved` to + `BreadcrumbRowRender` (ctor + property), set from `row.IsScoredFallback` at `:228-239`. +2. `UtilitiesCS/OutlookObjects/Folder/BreadcrumbBridgeMessages.cs` — `RowToJson` (`:315-336`) writes + the field; `ParseRows` (`:350-367`) reads it. This file is 463 lines. +3. `QuickFiler/Resources/FolderBreadcrumb.html` — CSS class plus `rowElement.className` composition + in `makeRow` (`:280-283`). This file is 491 lines. +4. `QuickFiler.Test/Viewers/FolderBreadcrumbAssetContractTests.cs` — the asset contract test asserts + against the compiled resource string (`:19`), so it must be extended. + +Efc list surface: +5. `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` — add the flag. +6. `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs` — set it in the `segments.Count == 0` + branch at `:124-131`. +7. `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` — emit the class at `:142`. +8. `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` — the CSS rule. + +That is 8 files for the "distinguishable" half of AC7 alone. AC7 offers "(or filtered)" as an +alternative; filtering the unresolved label out of the presented row set is a 1-file change and is +defensible, because a label that resolves to no snapshot node names a folder that no longer exists +and therefore cannot be filed to. Recommend the planner make this an explicit spec decision rather +than assuming the 8-file path. + +--- + +## R6. AC8 — the leading underscore. DEFINITE CONCLUSION + +**Conclusion reached: NO code path alters a leading underscore. The space in `_ Active Projects` was +introduced by the maintainer's transcription, not by the renderer.** + +There are TWO independent breadcrumb documents in this repository, and both were traced end to end. + +### Path 1 — QuickFiler item-view drop-down (the surface in the repro steps) + +Document: `QuickFiler/Resources/FolderBreadcrumb.html` (static asset), loaded via +`QuickFiler.Properties.Resources.FolderBreadcrumb` at +`QuickFiler/Viewers/ItemViewer.Breadcrumb.cs:118` and `:202`. + +1. C# projection: `BreadcrumbRenderProjection.ProjectRow` + (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbRenderProjection.cs:176-205`). Segment text is + either `segment.DisplayName` verbatim or `SplitVerbatim(row.VerbatimText)` (`:242-246`), which is + `string.Split(new[]{'\\'}, RemoveEmptyEntries)`. `Split` removes separators only; it inserts + nothing. `_Active Projects` contains no `\` and survives as one part. +2. JSON: `BreadcrumbBridgeSerializer.Serialize` -> `RowToJson` + (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbBridgeMessages.cs:252-336`) builds a Newtonsoft + `JObject` and emits `root.ToString(Formatting.None)` (`:309`). Newtonsoft escapes `"`, `\`, and + control characters only; `_` and `U+0020` are emitted literally and `Formatting.None` adds no + whitespace inside string values. +3. JS render: `makeCell` at `FolderBreadcrumb.html:248-258` sets + `element.textContent = cell.text;` and `element.title = cell.text;`. `textContent` performs no + entity decoding, no escaping, and no transformation. +4. CSS: the full stylesheet is `FolderBreadcrumb.html:10-159`. `.seg` (`:123-128`) is + `flex / overflow:hidden / text-overflow:ellipsis / white-space:nowrap`. There is no + `letter-spacing`, no `word-spacing`, no `text-transform`, no `::first-letter`, and no + `word-break` anywhere in the file. The font is `"Segoe UI", sans-serif` at 13px (`:54-55`). + +### Path 2 — Efc view + +Document generated server-side by `BreadcrumbHtmlRenderer.RenderDocument` +(`UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs:32-52`). + +1. Segment text is emitted by `AppendSegment` (`:175-190`): + `.Append(WebUtility.HtmlEncode(segment.DisplayName))`. `WebUtility.HtmlEncode` encodes + `& < > "` and non-ASCII above the encoder threshold. `_` (U+005F) and space (U+0020) are passed + through unchanged; it emits no ` `. +2. CSS: `BreadcrumbDocumentAssets.BaseCss/LightThemeCss/DarkThemeCss` + (`UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs:17-41`) — 15 rules total, none of + which is `letter-spacing`, `word-spacing`, `text-transform`, `::first-letter`, or `word-break`. +3. `BridgeJs` (`:49-145`) inserts child names with `div.textContent = c.displayName;` (`:136`) and + row HTML with `innerHTML = msg.html` (`:127`) where `msg.html` is the already-encoded + renderer output. + +### Repository-wide negative search + +`Grep` over `{UtilitiesCS,QuickFiler,ToDoModel,TaskMaster,Tags,TaskVisualization}/**/*.{cs,html,css,js}` +for the pattern `Replace\("_|Replace\('_|letter-spacing|text-transform|first-letter|word-break|word-spacing` +returned **"No files found"**. There is no code anywhere in the product that rewrites an underscore +or applies a spacing/casing transform to rendered text. + +### Transformations inspected and explicitly ruled out + +`string.Split` (removes separators only) - Newtonsoft string escaping (`\`, `"`, control chars only) - +`WebUtility.HtmlEncode` (`& < > "` only) - JS `textContent` assignment (no entity decoding) - +`WinForms` mnemonic prefixing (the WinForms prefix character is `&`, not `_`, so no combo-box or +owner-draw path can be responsible either) - CSS `letter-spacing` / `word-spacing` / +`text-transform` / `::first-letter` / `word-break` (none present in either document). + +**AC8 therefore requires no code change. The correct outcome is to record the verification and close +AC8 as "verified, renderer does not alter the leading underscore".** + +--- + +## R7. Test inventory + +All paths are repository-relative and were confirmed to EXIST via `Glob` and via their `` +entries. + +| Subject | Test file | Project | Status | +|---|---|---|---| +| `ArchiveStemContract` | `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemContractTests.cs` | UtilitiesCS.Test | Exists (`csproj:281`) | +| `FolderPredictor` incl. `GetOlSubpath` | `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs` (`:577-591` covers `GetOlSubpath`) | UtilitiesCS.Test | Exists (`csproj:401`) | +| `FolderPredictor` (coverage fill) | `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorCoverageExpansionTests.cs` | UtilitiesCS.Test | Exists (`csproj:400`) | +| `FolderPredictor.GetOlSubpath` (second suite) | `ToDoModel.Test/Email Utilities/FolderHandlerTests_Written.cs` | ToDoModel.Test | Exists (`csproj:74`) | +| `FolderPredictor.ProjectSuggestionPath` (direct) | — | — | **ABSENT**. Member is private; only indirectly exercised. Searched: `Grep` for `ProjectSuggestionPath` across `*.cs` -> only comments in `QfcItemController.FolderHandlingTests.Part2.cs:150,248` | +| `FolderPredictor.AddRecents` / `AddRecentRows` (direct) | — | — | **ABSENT**. `Grep` for `AddRecents` in `*.cs` produced no test call | +| `BreadcrumbRowBuilder` | `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowBuilderTests.cs` | UtilitiesCS.Test | Exists (`csproj:280`) | +| `FolderTreeSnapshotQueries` (general) | `UtilitiesCS.Test/OutlookObjects/Folder/FolderTreeSnapshotQueriesTests.cs` | UtilitiesCS.Test | Exists (`csproj:314`) | +| `FolderTreeSnapshotQueries.GetAncestorChain` | `UtilitiesCS.Test/OutlookObjects/Folder/FolderTreeSnapshotQueriesAncestorChainTests.cs` | UtilitiesCS.Test | Exists (`csproj:303`) | +| `QfcItemController` folder handling | `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.cs` and `.Part2.cs` (`Part2:213-241` pins `ProjectPredeterminedFolder`) | QuickFiler.Test | Exists (`csproj:181,182`) | +| `QfcItemController` suggestions | `QuickFiler.Test/Controllers/QfcItemController.FolderSuggestionsTests.cs` | QuickFiler.Test | Exists | +| `EfcFormController` breadcrumb binding | `QuickFiler.Test/Controllers/EfcFormControllerTests.cs` (`:62-127`) and `EfcFormControllerTests.Part2.cs` (`:236-259`) | QuickFiler.Test | Exists (`csproj:121,122`) | +| Efc router bind/join | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTests.cs`, `...QueueTests.cs`, `...QueueTests.Part2.cs`, `...Issue439Tests.cs`, `...Issue439Tests.Activation.cs`, `...Issue614Tests.cs`, `...Issue637Tests.cs`, `...Tests.Selection.cs` | QuickFiler.Test | Exist | +| `OutlookFolderHierarchyProvider` | `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` | UtilitiesCS.Test | Exists (`csproj:304`) | +| `IFolderHierarchyProvider` adapter | `UtilitiesCS.Test/OutlookObjects/Folder/FolderHierarchyProviderAdapterTests.cs` | UtilitiesCS.Test | Exists (`csproj:302`) | +| `FolderMinimalWrapper` | `UtilitiesCS.Test/OutlookObjects/Folder/FolderMinimalWrapperTests.cs` | UtilitiesCS.Test | Exists (`csproj:354`) | +| `FolderWrapper` | `UtilitiesCS.Test/OutlookObjects/Folder/FolderWrapperCoverageExpansionTests.cs`, `FolderWrapperStateTests.cs`, `FolderWrapperTraversalTests.cs`, plus five comparer suites | UtilitiesCS.Test | Exist (`csproj:403` and neighbours) | +| `SortItemsToExistingFolder` | `ToDoModel.Test/Email Utilities/SortItemsToExistingFolderTests.cs` and `SortItemsToExistingFolderTests_Unfinished.cs` | ToDoModel.Test | Files exist (`csproj:75,76`) but exercise **nothing** in the production type: the class under test is not compiled, and the only reference (`SortItemsToExistingFolderTests.cs:62`) is commented out. The `_Unfinished` class is named `Disabled_...` (`:12`) | +| `BreadcrumbRenderProjection` | `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRenderProjectionTests.cs`, `BreadcrumbRenderProjectionSelectorTests.cs` | UtilitiesCS.Test | Exist (`csproj:291,292`) | +| `BreadcrumbHtmlRenderer` | `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererTests.cs` | UtilitiesCS.Test | Exists (`csproj:278`) | +| QFC page asset contract | `QuickFiler.Test/Viewers/FolderBreadcrumbAssetContractTests.cs` | QuickFiler.Test | Exists (`csproj:107`) | +| `FolderBreadcrumbBridgeRouter` (sibling-owned) | `UtilitiesCS.Test/OutlookObjects/Folder/FolderBreadcrumbBridgeRouterTests.cs`, `...EdgeTests.cs`, `...InFlightTests.cs`, `...ReplaceItemsTests.cs`, `FolderBreadcrumbRouterSelectionConcurrencyTests.cs` | UtilitiesCS.Test | Exist (`csproj:285-289`) | +| `ArchiveStemProjection` (new) | — | — | ABSENT by definition; to be created | +| `ArchiveChainProjection` (new) | — | — | ABSENT by definition; to be created | + +--- + +## R8. Project compile entries (all non-SDK-style; every `.cs` must be listed) + +### `UtilitiesCS/UtilitiesCS.csproj` + +```xml + + + + + + + +``` + +**`FolderWrapper` IS listed**, under the exact Include string `OutlookObjects\Folder\FolderWrapper .cs` +— note the single space before `.cs`, written literally with NO quoting, escaping, or `%20`. +Any new file added here follows the same one-line self-closing form. + +### `QuickFiler/QuickFiler.csproj` + +```xml + + + + + +``` + +### `ToDoModel/ToDoModel.csproj` + +```xml + +``` + +That single line is the project's ENTIRE `Email Utilities` membership (verified by reading +`:118-152`, the complete `Compile` ItemGroup). It also demonstrates the space-in-path form for a +DIRECTORY: `Email Utilities\...`, unquoted and unescaped. + +**`SortItemsToExistingFolder` is NOT listed.** Not in `ToDoModel.csproj`, and not in any other +`.csproj` (`Grep` for `SortItemsToExistingFolder\.cs` with `glob: *.csproj` -> "No matches found"). +The only occurrence is `ToDoModel/ToDoModel.csproj.bak:138`, a backup file that MSBuild never reads. + +### `UtilitiesCS.Test/UtilitiesCS.Test.csproj` + +```xml + + + + + +``` + +### `QuickFiler.Test/QuickFiler.Test.csproj` + +```xml + + + +``` + +### `ToDoModel.Test/ToDoModel.Test.csproj` (space-in-directory form for tests) + +```xml + + +``` + +--- + +## R9. Sibling separability + +The concurrent sibling owns the QuickFiler folder drop-down OPEN/CLOSE lifecycle and +selection-commit ordering, in the breadcrumb bridge router and the selection session. + +| File (repository-relative) | Lines | Classification | +|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs` | 489 | **BOTH** | +| `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.SearchPresentation.cs` | 97 | LIFECYCLE/SELECTION | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs` | — | LIFECYCLE/SELECTION | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.Highlight.cs` | — | LIFECYCLE/SELECTION | +| `QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs` | 487 (per its own doc comment) | LIFECYCLE/SELECTION | +| `QuickFiler/Viewers/BreadcrumbBridgeCoordinator.Search.cs` | 102 | LIFECYCLE/SELECTION | + +**`FolderBreadcrumbBridgeRouter.cs` is BOTH. Stated plainly, with the members on each side:** + +- ROW-TEXT-PROJECTION side (this item would touch these if the trim were placed here): + `SetSuggestionsAsync` (`:29-97`), `SetSuggestionFallbacks` (`:100-119`), `AddPlainRows` (`:156-168`), + `CreateFallbackRow` (`:247-257`), and the constructor `FolderBreadcrumbBridgeRouter(IFolderHierarchyProvider)` + (`:19-23`). +- LIFECYCLE/SELECTION side (the sibling's): + `OpenSelector` (`:185-186`), `MoveSelector` (`:188-189`), `CommitSelector` (`:191-192`), + `ActivateSelector` (`:194-195`), `ActivateSelectorSubfolder` (`:205-208`), `CancelSelector` + (`:210-211`), `GetSelectorState` (`:213`), `SelectRow` (`:178-179`), `SelectItem` (`:182-183`), + `Clear` (`:171-175`), `Mutate` (`:239-245`), `Transition` (`:267-276`), + `ReplaceRowsPreservingSession` (`:478-482`), and the `_selectionSession` field (`:14`). + +The two sides share the `_sync` lock (`:15`), the `_suggestionGeneration` counter (`:16`), and the +constructor. A ctor-signature change from this item would collide directly with any sibling edit. + +**This is precisely why the R3 recommendation places the AC1/AC2 trim in +`OutlookFolderHierarchyProvider`.** With that placement, this item touches NONE of the six rows +above. Verify at review time that the final diff contains no hunk in any of them. + +Files that could be mistaken for the sibling's but are NOT (they belong to the Efc surface, which has +no open/close lifecycle — the Efc list is always expanded): +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` (304 lines) — ROW-TEXT-PROJECTION for this item + (`BindRowsAsync`, `ToHierarchyPath`, `AttachSegmentKeys`). +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` — flagged: it contains `FetchChainAsync` + (`:50-80`, projection side) alongside Efc row-selection helpers. This item needs no edit there + under the recommended design, but a reviewer should confirm. +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` — LIFECYCLE-adjacent (Efc arrows). Do not + touch. + +--- + +## Numeric Derivation Evidence + +The only enumerable population this research asserts, and which the planner may lift into a spec +acceptance criterion, is the set of archive-root stripping sites named by issue #799. + +- **Complete Family**: every production C# member in this repository that derives an archive-relative + (or ancestor-relative) path by removing a configured root prefix from a full Outlook folder path, + restricted to the members enumerated by issue #799. +- **Exhaustive Search Scope**: all `.cs` files under `UtilitiesCS/`, `QuickFiler/`, `ToDoModel/`, + `TaskMaster/`, `Tags/`, `TaskVisualization/`, plus every `*.csproj` in the repository for + compile-membership. Both records cover every member of the declared family, not one named pattern. +- **Inclusion Rules**: the member must (i) accept or read a root path, (ii) produce a shortened path + string derived from a longer one, and (iii) be reachable from display, filing, persistence, or + search code. +- **Exclusion Rules**: root SELECTORS that return a folder rather than a shortened string; the + contract type itself (`ArchiveStemContract`); test code; documentation; `.bak` files. + +**Primary Search Strategy** — member-name enumeration. Query expression: +`Grep pattern "ProjectSuggestionPath|ProjectPredeterminedFolder|GetOlSubpath" glob "*.cs"`, plus +targeted reads of `FolderPredictor.cs:740-990`, `QfcItemController.FolderHandling.cs:200-313`, +`FolderMinimalWrapper.cs:55-115`, `FolderWrapper .cs:180-240`, +`ToDoModel/Email Utilities/SortItemsToExistingFolder.cs:1-403`. + +**Primary Member Set** (normalized as `File::Member`): +1. `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs::ProjectSuggestionPath` +2. `QuickFiler/Controllers/QfcItemController.FolderHandling.cs::ProjectPredeterminedFolder` +3. `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs::AddRecents` +4. `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs::GetOlSubpath` +5. `UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs::ToRelativePath` +6. `UtilitiesCS/OutlookObjects/Folder/FolderWrapper .cs::LoadRelativePath` +7. `ToDoModel/Email Utilities/SortItemsToExistingFolder.cs::MASTER_SortEmailsToExistingFolder` + +**Primary Count**: 7 + +**Cross-check Search Strategy** — root-symbol occurrence enumeration, a different expression over a +different axis (the root value rather than the member name). Query expression: +`Grep pattern "ArchiveRootPath|OlRoot\.FolderPath|archiveRoot" glob "{UtilitiesCS,QuickFiler,ToDoModel,TaskMaster,Tags,TaskVisualization}/**/*.cs"`, +then applying the inclusion/exclusion rules to each of the 80 returned hits. + +**Cross-check Member Set** (normalized identically): +1. `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs::ProjectSuggestionPath` (hit at `:855`) +2. `QuickFiler/Controllers/QfcItemController.FolderHandling.cs::ProjectPredeterminedFolder` (hits at `:272,274,279`) +3. `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs::AddRecents` (reached via the `_globals.AF.RecentsList` read at `:790,793`; no root symbol appears, which is itself the defect) +4. `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs::GetOlSubpath` (root arrives as `olAncestor`, assigned from `ArchiveRootPath` at `:913` and `:751`) +5. `UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs::ToRelativePath` (hits at `:66,74,84`) +6. `UtilitiesCS/OutlookObjects/Folder/FolderWrapper .cs::LoadRelativePath` (hits at `:204,212,222`) +7. `ToDoModel/Email Utilities/SortItemsToExistingFolder.cs::MASTER_SortEmailsToExistingFolder` (hits at `:67,91,107,109`) + +**Cross-check Count**: 7 + +**Member-set Comparison**: the normalized primary and cross-check member sets are IDENTICAL — same +seven `File::Member` pairs, no member present in one and absent from the other. + +The cross-check additionally surfaced members that the inclusion rules REJECT and that are therefore +not part of the count: `UtilitiesCS/OutlookObjects/MailItem/MailItemHelper.Loading.cs::ResolveFolderRoot` +(returns a `Folder`, not a shortened string — root selector), `QuickFiler/Controllers/EfcDataModel.cs::TryGetArchiveRoot` +(accessor only), `UtilitiesCS/OutlookObjects/Folder/FolderConverter.cs` (already on the #614 +contract), `TaskMaster/AppGlobals/ArchiveRootPathGuard.cs` (validator), and +`QuickFiler/Controllers/BreadcrumbBridgeRouter.cs::ToHierarchyPath` (already on the contract, and it +PREFIXES rather than strips). These rejections are recorded so the exhaustive scope is auditable. + +**Assertion released**: the archive-root stripping family named by issue #799 has exactly **seven** +members, and of those, four are recommended for CONVERT (a, b, c, d) and three for LEAVE ALONE +(e, f, g). + +--- + +## Behavior semantics (AC-by-AC) + +- **AC1** — success: for every Suggestion row and every SearchResult row on both surfaces, the first + rendered segment is the first folder BELOW the archive root, arrows and clickable ancestors are + produced by the existing `BreadcrumbRenderProjection` / `BreadcrumbHtmlRenderer` cell pipeline + unchanged. Failure: any row whose first segment is the mailbox or `Archive`. + Ordering: trimming happens after chain resolution and before row construction, so row order, + banner placement, and the trash pseudo-row are untouched. +- **AC2** — success: chain contains a node whose `FolderPath` equals the archive root, and that node + is not the leaf. Failure paths: (i) no such node -> `logger.Error` once, empty chain, existing + fallback; (ii) chain equals the root exactly (leaf IS the root) -> same treatment; (iii) empty + chain from the snapshot -> existing behavior, no new log. +- **AC3** — invariant: `BreadcrumbStateRow.WithFilingTarget` (`BreadcrumbStateModel.Row.cs:77-98`) + substitutes the presented stem into the LEAF segment's `FolderPath` only. Trimming removes LEADING + segments, so the leaf and therefore the filing value are untouched. Same for + `BreadcrumbRow.FilingTarget` (`BreadcrumbRow.cs:63-65`). +- **AC4** — the empty-root one-separator strip disappears because `TryMakeArchiveRelative` returns + `false` for a whitespace-only root (`ArchiveStemContract.cs:113`). +- **AC5** — both `AddRecents` and `AddRecentRows` project. +- **AC6** — score keys and row texts are projected by the same function against the same root. +- **AC7** — one error per distinct label per provider instance; stale rows rendered + distinguishably or filtered (spec decision required, see R5). +- **AC8** — verified, no change (R6). + +## Testing implications (no test code written) + +Consistent with `.claude/rules/general-unit-test.md` and the C# unit test policy (MSTest + Moq + +FluentAssertions, no temp files, no external dependencies): + +- `ArchiveStemProjection.ToDisplayStem`: pure table tests over the #614 boundary cases — under root, + equal to root, `Archive2`, trailing-separator root, empty root, null/empty path, forward-slash + separators, mixed case. +- `ArchiveChainProjection.TryTrimBelowArchiveRoot`: chain through root; chain NOT through root; chain + whose leaf IS the root; empty chain; single-element chain; root supplied with a trailing separator. + All constructible from `FolderBreadcrumbSegment` literals with no snapshot and no COM. +- `OutlookFolderHierarchyProvider`: a `Mock` returning a hand-built + `FolderTreeSnapshot` (the pattern already used throughout + `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs`) plus a + `Func` accessor; assert the trimmed chain, and assert the AC7 gate by invoking the same + unresolvable label twice and counting log emissions through an injected sink or a log4net memory + appender. Prefer a delegate sink over an appender to keep the test independent of global log4net + configuration. +- `BreadcrumbBridgeRouter.BindRowsAsync` score projection: existing router test fixtures in + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs` already build rooted/relative + target pairs; extend with a rooted-score/relative-row case asserting a non-empty `percentText`. +- `FolderPredictor` recents: exercise `FolderRowArray` and `FolderArray` on a predictor whose + `_globals.AF.RecentsList` holds one rooted and one relative entry; assert text parity between the + two properties (the parity contract is documented at `FolderPredictor.cs:233-242` and is currently + unasserted). +- `QfcItemController.FolderHandlingTests.Part2.cs:213-241` must be UPDATED, not preserved: its + empty-root assertion (`:220-223`) encodes the behavior AC4 removes. +- Integration scenario from the issue (banner + suggestion + search result + trash + stale label) + can be driven entirely through `BreadcrumbRowBuilder.BuildRows` and + `BreadcrumbRenderProjection.Project` with no WebView2 and no Outlook. + +--- + +## R10. Write set + +### Production sources + +`UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` (CREATE) +`UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` (CREATE) +`UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` (MODIFY — 141 lines, ample room) +`UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` (MODIFY — 1003 lines, already over the ceiling; the change is net line-NEUTRAL or negative because `ProjectSuggestionPath`'s 13-line body collapses to a delegation) +`QuickFiler/Controllers/QfcItemController.FolderHandling.cs` (MODIFY — 313 lines) +`QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` (MODIFY — 304 lines; AC6 score projection) +`QuickFiler/Controllers/EfcFormController.cs` (MODIFY — 1320 lines; ONE argument added at the provider construction on line 1053-1055) +`QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` (CREATE) +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` (MODIFY) + +**Hard constraint the planner must honour**: `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` +is **exactly 500 lines** — at the repository ceiling, not near it. The provider construction at +`:147-149` must gain one argument, which CSharpier will format as an additional line (the collapsed +single-line call is ~126 columns including indent, well past the print width). The compensating move +is to relocate `EnsureBreadcrumbPipeline` (`:132-163`, 32 lines including its comment and the +`[ExcludeFromCodeCoverage]` attribute) into the new partial +`QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`, leaving `ViewerSetup.cs` at roughly +468 lines. That relocation is the reason both files appear in the write set. + +If AC7 takes the "rendered distinguishably" branch rather than the "filtered" branch, add: + +`UtilitiesCS/OutlookObjects/Folder/BreadcrumbRenderProjection.cs` +`UtilitiesCS/OutlookObjects/Folder/BreadcrumbBridgeMessages.cs` +`UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` +`UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs` +`UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` +`UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` +`QuickFiler/Resources/FolderBreadcrumb.html` + +### Test sources + +`UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` (CREATE) +`UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` (CREATE) +`UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` (CREATE — a new file rather than growing `OutlookFolderHierarchyProviderTests.cs`) +`UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` (CREATE) +`QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` (MODIFY — the empty-root assertion at `:220-223` encodes behavior AC4 removes) +`QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` (CREATE — AC6) + +If the "rendered distinguishably" branch is taken, add: + +`QuickFiler.Test/Viewers/FolderBreadcrumbAssetContractTests.cs` (MODIFY) +`UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRenderProjectionTests.cs` (MODIFY) + +### Project files + +`UtilitiesCS/UtilitiesCS.csproj` +`UtilitiesCS.Test/UtilitiesCS.Test.csproj` +`QuickFiler/QuickFiler.csproj` +`QuickFiler.Test/QuickFiler.Test.csproj` + +### Paths containing a SPACE + +This item recommends that neither space-containing path be touched, so neither appears in the write +set above. They are restated here in prose, without backticks, so a downstream path extractor does +not misread them as write targets: + +- The ToDoModel email-utilities sort file — directory ToDoModel, then the directory named Email + Utilities (one space between the two words), then the file SortItemsToExistingFolder.cs. NOT to be + modified: it is not a Compile item in ToDoModel.csproj and has no live caller. +- The folder wrapper source file — directory UtilitiesCS, then OutlookObjects, then Folder, then the + file named FolderWrapper followed by one space and then .cs. NOT to be modified: 532 lines + (already over the 500-line ceiling) and its full-path fallback is depended upon by the persisted + RelativePath restore and the classifier corpus. + +Also NOT to be modified, written without backticks for the same reason: the file UtilitiesCS, +OutlookObjects, Folder, FolderMinimalWrapper.cs — its RestoreFromRelativePath branch depends on the +divergent full-path fallback. + +### Explicitly out of scope and absent from the write set + +Nothing under the dot-claude, dot-codex, or dot-agents trees. Neither of the two published JSON files +under the config directory. No GitHub workflow file. Not the solution file. Not the repository-root +build property files. + +**The fix does not require the solution file or any repository-root build property file.** Every new +`.cs` is added to an existing project via an explicit `` line, and no new project is +introduced. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md index 66dcc10ae..b20fdaf67 100644 --- a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md @@ -3,18 +3,81 @@ - **Issue:** #799 - **Parent (optional):** none - **Owner:** drmoisan -- **Last Updated:** 2026-09-06T22-01 +- **Last Updated:** 2026-09-06T18-40 - **Status:** Draft -- **Version:** 0.1 +- **Version:** 1.0 +- **Work Mode:** full-bug. This spec is the sole authoritative acceptance-criteria source. There is no + user-story.md in this folder, and none is to be created. + +## Path notation convention + +Backticks in this document are reserved for repository paths that this change will actually create or +modify. Every one of those paths appears in the Write Set section below, written with forward slashes. +Every other file reference in this document — comparisons, precedents, citations, out-of-scope files, +and files this change deliberately leaves alone — is written as plain prose with no backticks, +including its File.cs:123 line citation. + +This is not a style preference. Get-BlastRadius in .claude/lib/blast-radius/BlastRadius.psm1 is given +both the atomic-plan text and this spec text, and it harvests path tokens from inline code spans in +both. The extraction and classification rules are in .claude/lib/blast-radius/BlastRadiusExtraction.psm1: +Get-InlineCodeToken splits every backtick span on whitespace, and Get-PathTokenKind accepts a token as +a concrete repository path when it contains a forward slash, is not absolute, carries no placeholder +marker, and ends in a recognized extension such as cs, csproj, md, ps1 or json. An accepted token is +added to this item's blast radius exactly as if the change had written that file, which makes this item +contend with concurrent sibling items that touch the same file. The extractor has no notion of +polarity: a backticked path inside a sentence saying the change will not touch it is read as a write +claim in exactly the same way. + +Two consequences are load-bearing for this item and are applied throughout: + +- No path in this document is written with angle brackets, dollar-brace, dollar-paren, or a percent + sign when a real write claim is meant. Write Set entries are concrete. +- The two repository paths that contain a space are not in the Write Set and carry no backticks. They + are restated in words in the Write Set section. + +## Specification change, not a regression + +The behaviour this specification removes — the full root-to-leaf ancestor lineage rendered on +suggestion rows, beginning at the mailbox store node — is exactly what issue #439 delivered. It was +correct against #439's Expected Behavior, and it is being deliberately superseded here, not repaired. + +A later reader must not read this change as a regression fix against #439. The maintainer settled the +new rendering rule on 2026-09-06: for every row kind, on both surfaces, the lineage begins at the first +segment below the archive root. + +The #439 constraint that is **not** changing is that the filing target and the score-lookup key remain +the archive-relative stem. Trimming affects display segments only. That constraint is carried forward +unchanged as AC3 in the Acceptance Criteria section below, and it is the invariant that every part of +the design is checked against. + +Because #439's behaviour is currently pinned by existing tests, those tests must be **retargeted**, not +merely extended. Retargeting means the assertion is rewritten to encode the new rule against a provider +configured the way production configures it; deleting the test or leaving it asserting the superseded +rule against a deliberately disabled configuration are both unacceptable, because either outcome leaves +the suite pinning nothing. The tests concerned are named in the Write Set section under test sources: +the provider chain test, the #439 Efc router tests, and the AC4 empty-root assertion in the +QuickFiler item-controller folder-handling tests. ## Context -In QuickFiler (ordinary and High Confidence modes) suggestion rows render their breadcrumb lineage from the store root, for example `dmoisan@realgoodfoods.com -> Archive -> _Active Projects -> Build RGF Org and Team -> Sales Lead`, while typed search-result rows render `_Active Projects -> Build RGF Org and Team -> Sales Lead`. The mailbox and Archive segments are superfluous: every filing target is under the archive root, and the lineage must begin at the first segment below it for every row kind. The full-root lineage is the behavior delivered by issue #439 ("full root-to-leaf ancestor lineage"), so this is a specification change, not a regression. The same change should unify several archive-root stripping paths that have drifted into duplicates and gaps. + +In QuickFiler (ordinary and High Confidence modes) suggestion rows render their breadcrumb lineage from +the store root — mailbox, then Archive, then _Active Projects, then Build RGF Org and Team, then Sales +Lead — while typed search-result rows render only _Active Projects, Build RGF Org and Team, Sales Lead. +The mailbox and Archive segments are superfluous: every filing target is under the archive root, and +the lineage must begin at the first segment below it for every row kind. The same change unifies +several archive-root stripping paths that have drifted into duplicates and gaps. + +Throughout this document the word "mailbox" stands in for the account name of the default Outlook +store. The literal address is not reproduced here. Environment: - OS/version: Windows 11 Pro 10.0.26200 -- Runtime: .NET Framework 4.8 VSTO Outlook add-in with WebView2 breadcrumb, debug build from `TaskMaster\bin\Debug`, HEAD `c431dc32` (2026-09-06) -- Command/flags used: Outlook ribbon -> QuickFiler and QuickFiler High Confidence; folder drop-down in the item view (`ItemViewer` breadcrumb, `FolderBreadcrumb.html`) -- Data source or fixture: live mailbox; archive root resolves to `\\dmoisan@realgoodfoods.com\Archive` (`AppOlObjects.ArchiveRootPath`) +- Runtime: .NET Framework 4.8 VSTO Outlook add-in with a WebView2 breadcrumb; debug build from + TaskMaster\bin\Debug at HEAD c431dc32 (2026-09-06) +- Command/flags used: Outlook ribbon, QuickFiler and QuickFiler High Confidence; the folder drop-down + in the item view (the ItemViewer breadcrumb, backed by the FolderBreadcrumb.html page asset) +- Data source or fixture: live mailbox. The archive root resolves to the default store root followed by + the literal folder Archive, exposed as AppOlObjects.ArchiveRootPath. Impact / Severity: - [ ] Blocker @@ -22,110 +85,751 @@ Impact / Severity: - [x] Medium - [ ] Low -Display-only: filing lands correctly. The redundant segments consume most of the row width and make same-named folders harder to distinguish, which is the problem #439 set out to solve. - +Display-only: filing lands correctly. The redundant segments consume most of the row width and make +same-named folders harder to distinguish, which is the problem #439 set out to solve. ## Repro & Evidence + Steps to Reproduce: 1. Launch QuickFiler on Inbox. Open the folder drop-down on an item without typing. -2. Observe the first suggestion row: `dmoisan@realgoodfoods.com -> Archive -> _Active Projects -> Build RGF Org and Team -> Sales Lead`. -3. Type a few letters into the search box. Observe the same folder as a search result: `_Active Projects -> Build RGF Org and Team -> Sales Lead`. Typing `90` shows `_Active Projects -> 90 Day Plan`. -4. Accept a suggestion: filing lands in the correct Outlook and file-system folders (the filing target is the archive-relative stem, not the displayed lineage). +2. Observe the first suggestion row: mailbox, then Archive, then _Active Projects, then Build RGF Org + and Team, then Sales Lead. +3. Type a few letters into the search box. Observe the same folder as a search result: _Active + Projects, then Build RGF Org and Team, then Sales Lead. Typing 90 shows _Active Projects, then 90 + Day Plan. +4. Accept a suggestion: filing lands in the correct Outlook and file-system folders. The filing target + is the archive-relative stem, not the displayed lineage. -Note: the maintainer's transcription of the search row showed `_ Active Projects` with a space after the underscore. The orchestrator should verify whether the renderer alters a leading underscore or whether this was a transcription artifact. +Note carried from the issue: the maintainer's transcription of the search row showed a space after the +leading underscore. That question is resolved in decision D-C below and by AC8. Expected: -- Every row in the folder list, suggestion or search result, renders as `_Active Projects -> Build RGF Org and Team -> Sales Lead`: the lineage begins at the first segment below the archive root, each segment is clickable for ancestor navigation, and no row shows the mailbox or Archive segments. -- A resolved ancestor chain that does not pass through the archive root node is logged as an error and rendered with the existing single-segment fallback, never with a mailbox prefix. There are no legitimate filing targets outside the archive root. -- One archive-root projection rule is used everywhere a suggestion path is prepared for display or compared against displayed entries. +- Every row in the folder list, suggestion or search result, renders beginning at the first segment + below the archive root, each segment is clickable for ancestor navigation, and no row shows the + mailbox or Archive segments. +- A resolved ancestor chain that does not pass through the archive root node is logged as an error and + rendered with the existing single-segment fallback, never with a mailbox prefix. There are no + legitimate filing targets outside the archive root. +- One archive-root projection rule is used everywhere a suggestion path is prepared for display or + compared against displayed entries. Actual: -- Suggestion rows show the mailbox and Archive segments; search rows do not. Ordinary and High Confidence modes behave the same. +- Suggestion rows show the mailbox and Archive segments; search rows do not. Ordinary and High + Confidence modes behave the same. - Recent-folder entries are appended to the suggestion list with no projection at all. -- The Efc breadcrumb binding joins projected row text against raw scorer paths, so an archive-rooted suggestion loses its percentage. -- Several suggestion rows fail hierarchy resolution and fall back to a single segment, with errors such as `No snapshot node path ends with '\Forums\Pricing'; leaving 'Forums\Pricing' unresolved.` +- The Efc breadcrumb binding joins projected row text against raw scorer paths, so an archive-rooted + suggestion loses its percentage. +- Several suggestion rows fail hierarchy resolution and fall back to a single segment. Logs / Screenshots: - [x] Attached minimal logs or screenshot -- Snippet (`TaskMaster\bin\Debug\logs\debug_2026-09-06.log`), stale-label resolution failures: +- Snippet from the debug log in the debug output tree (TaskMaster\bin\Debug\logs\debug_2026-09-06.log), + stale-label resolution failures: ``` 2026-09-06 16:45:51,908 [VSTA_Main] ERROR UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider - No snapshot node path ends with '\Scorecards\Monthly Scans'; leaving 'Scorecards\Monthly Scans' unresolved. 2026-09-06 16:45:51,937 [VSTA_Main] ERROR UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider - No snapshot node path ends with '\Forums\Pricing'; leaving 'Forums\Pricing' unresolved. ``` +Research counted 18 occurrences of that error family in one session across only two distinct labels, +Scorecards\Monthly Scans and Forums\Pricing, which confirms once-per-render rather than once-per-label +emission. The same log also shows the Efc route reaching the same resolver with a fully rooted path +after the Efc router re-roots a relative presented target, so the AC7 gate must cover both surfaces. ## Scope & Non-Goals + - In scope: -- Out of scope / non-goals: + - The archive-root chain trim for AC1 and AC2, placed in the hierarchy provider's ancestor-chain + method. + - One shared display projection built on ArchiveStemContract.TryMakeArchiveRelative, replacing the + two duplicated stripping members named by AC4 and applied to the recents append, the recents row + mirror, and the include-children branch of the search-subpath helper. + - The Efc score-path projection for AC6, applied in the Efc router where the bound root is already + known and normalized. + - The once-per-label-per-provider stale-label log gate and the zero-candidate row suppression for + AC7, per decision D-B. + - The AC8 verification finding, recorded in decision D-C. No renderer change. + - MSTest coverage for the two new pure helpers, the trimmed provider, the recents projection and the + Efc score join, plus the retargeting of the tests that pin superseded behaviour. + - The explicit Compile Include entries every new .cs file needs in these legacy non-SDK projects. + +- Out of scope / non-goals. The paths below are deliberately unbackticked; they are not part of the + change footprint: + - Anything under the dot-claude, dot-codex or dot-agents trees. No governance, skill, hook, rule or + library file is required by this fix. + - The two published JSON files under the config directory (the blast-radius module map and the + orchestration routing map). Research confirmed neither is required. + - Every GitHub workflow file under .github/workflows. No CI gate changes. + - The solution file TaskMaster.sln, and the repository-root build property files. Every new .cs is + added to an existing project through an explicit Compile Include line and no new project is + introduced, so neither the solution nor any root build property file is touched. + - UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs and the folder wrapper source file — see + decision D-A, LEAVE. + - The ToDoModel email-utilities sort file — see decision D-A, LEAVE. + - UtilitiesCS/OutlookObjects/MailItem/MailItemHelper.Loading.cs:122-130, ResolveFolderRoot. Research + found this unanchored archive-root comparison during the cross-check. It selects a root folder + rather than stripping a prefix, so it is outside the family AC4 names and outside this fix. + - The sibling-owned files listed in decision D-D: the UtilitiesCS breadcrumb bridge router and its + search-presentation partial, the breadcrumb selection session and its highlight partial, and the + QuickFiler breadcrumb bridge coordinator and its search partial. + - UtilitiesCS/OutlookObjects/Folder/FolderTreeSnapshotQueries.cs. GetAncestorChain keeps returning + the full store-rooted chain; the trim is applied above it, so the query stays a faithful snapshot + walk and its existing tests stay green on their own terms. + - Explicitly excluded systems, integrations, or datasets: + - The persisted classifier corpus and the persisted subject map. This change does not rewrite, + migrate or re-shape any stored label. The two LEAVE decisions in D-A exist specifically to keep + persisted-data round-tripping unchanged. + - Outlook COM automation in tests. Every new test is constructible from plain objects, a mocked tree + service and hand-built snapshots, with no live Outlook and no WebView2. + - The SpamBayes and Triage scoring engines. Scores are consumed, never recomputed. ## Root Cause Analysis -Why the two row kinds differ (verified by code read): - -- Suggestion rows: `FolderBreadcrumbBridgeRouter.SetSuggestionsAsync` decorates each row with the ancestor chain from `IFolderHierarchyProvider`. `FolderTreeSnapshotQueries.GetAncestorChain` (`UtilitiesCS\OutlookObjects\Folder\FolderTreeSnapshotQueries.cs:109-140`) walks `ParentKey` until it is null, i.e. up to the store node. `BreadcrumbRowBuilder.MapSegments` (`UtilitiesCS\OutlookObjects\Folder\BreadcrumbRowBuilder.cs:178-208`) maps every node to a segment with no trimming. This is the #439 design (`docs\features\active\2026-08-07-efcviewer-missing-lineage-and-segment-navigation-439\issue.md`, Expected Behavior). -- Search rows: `FolderBreadcrumbBridgeRouter.ReplaceItemsPreservingSession` (`FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55`) carries the verbatim archive-relative stem produced by `FolderPredictor.GetOlSubpath` (`FolderPredictor.cs:953-971`), and `BreadcrumbRenderProjection.SplitVerbatim` (`BreadcrumbRenderProjection.cs:242-246`) splits it on path separators for rendering. No provider chain, so no store segments. - -Archive-root projection paths that should be unified under `ArchiveStemContract` (`UtilitiesCS\OutlookObjects\Folder\ArchiveStemContract.cs`, the canonical ordinal, anchored, separator-terminated stripper from #614): - -- `FolderPredictor.ProjectSuggestionPath` (`FolderPredictor.cs:848-861`): private, backslash-only, appends `"\\"` to the root unconditionally, silently returns the full path on no match, and with an empty root strips one leading separator from any path. -- `QfcItemController.ProjectPredeterminedFolder` (`QuickFiler\Controllers\QfcItemController.FolderHandling.cs:272-285`): a hand-copied duplicate of the above added by #678 AC12 because the original is private. -- `FolderPredictor.AddRecents` (`FolderPredictor.cs:788-795`): appends `_globals.AF.RecentsList` verbatim with no projection. -- `FolderPredictor.GetOlSubpath` (`FolderPredictor.cs:953-971`): blind `Substring` with no prefix verification. -- `FolderMinimalWrapper.ToRelativePath` (`FolderMinimalWrapper.cs:84`) and `FolderWrapper.RelativePath` (`FolderWrapper .cs:194-224`): unanchored `Replace(root + "\\", "")` with a full-path fallback when the root is null, which is how a rooted label can enter the persisted classifier corpus. -- `EfcFormController.BindBreadcrumbRowsAsync` (`QuickFiler\Controllers\EfcFormController.cs:1115-1118`): passes projected `rows` with raw `Suggestions.ToScoredArray()` scores to `BindRowsAsync`, which joins by presented-text equality, so archive-rooted suggestions lose their score. `AddSuggestionRows` (`FolderPredictor.cs:835-846`) projects the score path; this site does not. -- `ToDoModel\Email Utilities\SortItemsToExistingFolder.cs:67-109`: legacy unanchored `Contains(StrRoot)` and `Substring` root handling. - -Stale labels: the resolution failures logged above come from `OutlookFolderHierarchyProvider.ResolveByUniqueSuffix` (`OutlookFolderHierarchyProvider.cs:85-112`) when a persisted suggestion label (classifier corpus, subject map, or recents) names a folder that no longer exists at that path. Those rows fall back to a single segment. The change should make the fallback visibly distinct (or filter such labels) and log once per label rather than once per render. - -Runtime facts that constrain the design: `AppOlObjects.ArchiveRootPath` (`TaskMaster\AppGlobals\AppOlObjects.cs:260-270`, `AppOlObjects.ArchiveRoot.cs:86-93`) is derived from the default store root plus the literal folder `Archive`, validated by `ArchiveRootPathGuard`, and throws rather than returning null. Persisted suggestion labels are archive-relative when written correctly (`OlFolderClassifierGroup.cs:205`, `SubjectMapSco.Orchestration.cs:19-33`, `AppAutoFileObjects.cs:211-229`). +Why the two row kinds differ (verified by code read against this worktree): + +- Suggestion rows. FolderBreadcrumbBridgeRouter.SetSuggestionsAsync decorates each row with the + ancestor chain from IFolderHierarchyProvider. FolderTreeSnapshotQueries.GetAncestorChain, in + UtilitiesCS/OutlookObjects/Folder/FolderTreeSnapshotQueries.cs:109-146, walks ParentKey until it is + null, that is, up to the store node. BreadcrumbRowBuilder.MapSegments, in + UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:178-208, maps every node to a segment with + no trimming. This is the #439 design, recorded in the #439 feature folder under + docs/features/active/2026-08-07-efcviewer-missing-lineage-and-segment-navigation-439, Expected + Behavior. +- Search rows. FolderBreadcrumbBridgeRouter.ReplaceItemsPreservingSession, in + FolderBreadcrumbBridgeRouter.SearchPresentation.cs:38-55, carries the verbatim archive-relative stem + produced by FolderPredictor.GetOlSubpath at FolderPredictor.cs:953-971, and + BreadcrumbRenderProjection.SplitVerbatim at BreadcrumbRenderProjection.cs:242-246 splits it on path + separators for rendering. No provider chain, so no store segments. + +The snapshot genuinely contains the store node. OutlookFolderHierarchyReader.ReadStoreAsync, in +UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyReader.cs:117-160, pushes the store root itself +with an empty parent entry id, and ToNode at :177-204 resolves its parent key to null. A null parent +key therefore identifies exactly the store root and terminates the walk, which is why the rendered +lineage begins at the mailbox. + +The archive-root node is not identifiable by entry id, by a null parent key, or by a literal child +named Archive. The reliable identifier is the node's FolderPath compared ordinal-case-insensitively +against IOlObjects.ArchiveRootPath with trailing separators trimmed — precisely +ArchiveStemContract.TryMakeArchiveRelative's root handling. The node's RelativePath property is not +usable: OutlookFolderHierarchyReader.GetRelativePath at :206-211 makes it relative to the store root +using the same unanchored Replace pattern as the wrapper sites below. + +Archive-root projection sites that have drifted, and that AC4 and AC5 unify under ArchiveStemContract +(UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs, the canonical ordinal, anchored, +separator-terminated stripper from #614): + +- FolderPredictor.ProjectSuggestionPath, FolderPredictor.cs:848-861. Private, backslash-only, appends a + single backslash to the root unconditionally so a root that already ends in a separator can never + match, silently returns the full path on no match, and with an empty root strips exactly one leading + separator from any path. That last behaviour is the defect AC4 names. +- QfcItemController.ProjectPredeterminedFolder, + QuickFiler/Controllers/QfcItemController.FolderHandling.cs:272-285. A hand-copied duplicate of the + above, added by #678 AC12 solely because the original is private. Its own comment at :228-230 states + that reason, which disappears once the shared helper is public. +- FolderPredictor.AddRecents, FolderPredictor.cs:788-795. Appends the recents list verbatim with no + projection. The issue under-reports this site: the row-model mirror AddRecentRows at + FolderPredictor.cs:866-882 has the same gap at :879, and that row list is what the breadcrumb + surfaces actually consume. The row array's own XML documentation at FolderPredictor.cs:233-242 + asserts the string list and the row list are text-identical, so projecting only one of them would + break a documented contract. +- FolderPredictor.GetOlSubpath, FolderPredictor.cs:953-971. A blind Substring with no prefix + verification. If the path does not start with the ancestor the result is a garbage substring, and if + the path is no longer than the ancestor it throws ArgumentOutOfRangeException. The ancestor here is a + search root, not necessarily the archive root; TryMakeArchiveRelative is root-agnostic, so it serves + this site. +- FolderMinimalWrapper.ToRelativePath, UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs:56-86, + with the Replace at :84; and the folder wrapper's LoadRelativePath at :194-224, with a + character-identical body at :222. Both use an unanchored, case-sensitive Replace of the root plus a + separator, with a full-path fallback when the root is null. The issue names the wrapper member as + RelativePath; that is the persisted JSON accessor at :187-192, and the stripping actually lives in + the internal LoadRelativePath. The full-path fallback is how a rooted label can reach the persisted + classifier corpus. +- EfcFormController.BindBreadcrumbRowsAsync, QuickFiler/Controllers/EfcFormController.cs:1111-1128, + with the load-bearing lines at :1115-1118. It passes already-projected row text together with the + raw scorer output from Suggestions.ToScoredArray() into BindRowsAsync, which joins by presented-text + equality. BreadcrumbRowBuilder.BuildProbabilityIndex keys on the score's FolderPath at + BreadcrumbRowBuilder.cs:222-224 and BuildRow looks up the presented text at :133, so for an + archive-rooted suggestion the key is the rooted path while the presented text is the stem. The + lookup misses, the probability is null, and the percentage cell renders empty. By contrast + AddSuggestionRows at FolderPredictor.cs:835-846 projects both the row text and the score path, which + is why the QuickFiler surface does not show the defect. +- The ToDoModel email-utilities sort file at :67-109: legacy unanchored Contains and Substring root + handling, using a bitwise and instead of a short-circuit and, and taking a Substring against the + email root rather than the archive root. + +Stale labels. The resolution failures logged above come from +OutlookFolderHierarchyProvider.ResolveByUniqueSuffix, in +UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs:90-114 with the emission at +:108-112, when a persisted suggestion label from the classifier corpus, the subject map or the recents +list names a folder that no longer exists at that path. Those rows fall back to a single segment. Note +that the emission has two distinct causes in one message: zero candidates and multiple candidates. +That distinction is the basis of decision D-B. + +Runtime facts that constrain the design. AppOlObjects.ArchiveRootPath, in +TaskMaster/AppGlobals/AppOlObjects.cs:260-270 and TaskMaster/AppGlobals/AppOlObjects.ArchiveRoot.cs:86-93, +is derived from the default store root plus the literal folder Archive, validated by +ArchiveRootPathGuard, and throws InvalidOperationException rather than returning null when the root is +unresolvable. An existing test, QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242, +exists precisely because of that throw. Persisted suggestion labels are archive-relative when written +correctly, per OlFolderClassifierGroup.cs:205, SubjectMapSco.Orchestration.cs:19-33 and +AppAutoFileObjects.cs:211-229. + +## Spec Decisions + +Each decision below is settled by this specification. A planner implements it as written; a reviewer +checks the diff against it. + +### D-A. AC4 site disposition + +AC4 names only ProjectSuggestionPath and ProjectPredeterminedFolder as the pair to be replaced by one +shared projection built on ArchiveStemContract.TryMakeArchiveRelative, and additionally requires the +empty-root one-separator strip to be eliminated. That is implemented exactly as written. Research +enumerated seven drifted stripping sites; this decision records, site by site, which are converted and +which are deliberately left, so that a reviewer does not read a LEAVE as an oversight. + +**CONVERT (four sites):** + +1. **FolderPredictor.ProjectSuggestionPath.** Replaced by a delegation to the new shared display + projection. Both callers (AddSuggestions at FolderPredictor.cs:810 and AddSuggestionRows at :842) + are display paths. Rationale: it is the origin of the duplicated expression and the site that + carries the empty-root defect. Evidence: FolderPredictor.cs:848-861. +2. **QfcItemController.ProjectPredeterminedFolder.** Replaced by a one-line delegation to the same + helper, retaining the member so its existing test keeps a target. Rationale: its own comment states + it exists only because the original was private, and AC4 names it explicitly. Evidence: + QuickFiler/Controllers/QfcItemController.FolderHandling.cs:272-285 and its sole caller at :231-234. +3. **The recents append AND the row-model mirror.** Both FolderPredictor.AddRecents at + FolderPredictor.cs:788-795 and AddRecentRows at :866-882 project each recent entry through the same + helper. Rationale: the issue under-reports this site by naming only the string append. The row + mirror is the path the breadcrumb actually consumes, and the documented text-parity contract at + FolderPredictor.cs:233-242 would be broken if only one were projected. Evidence: the unprojected + row construction at FolderPredictor.cs:879. This is what AC5 requires. +4. **FolderPredictor.GetOlSubpath, the include-children true branch only.** Rationale: only that + branch is a stem strip and can be expressed as a verified prefix removal. The false branch computes + a leaf name, which the contract does not do; converting it would change a different function. + Evidence: FolderPredictor.cs:953-971, and the two existing public-caller assertions in the ToDoModel + folder-handler tests, which pin both branches and survive a contract-based rewrite of the true + branch unchanged. + +**LEAVE, with reasons (three sites):** + +5. **The folder minimal wrapper's relative-path loader**, UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs:56-86. + Left unchanged. Its RestoreFromRelativePath branch at :88-131 explicitly tests for a leading double + separator at :105-114 and walks the COM parent chain to recover a folder whose stored relative path + is in fact a full store path. That branch exists precisely because the loader can return a rooted + path. The property is JSON-persisted, so failing the loader closed — which is what the contract does + — would change persisted-data round-tripping for values already on disk. No acceptance criterion + covers it. +6. **The folder wrapper's relative-path loader**, at :194-224 in the folder wrapper source file (named + in words in the Write Set section). Left unchanged, for the same round-tripping reason, and + additionally because that file is 532 lines and already exceeds the repository's 500-line ceiling, + so any edit invites a split that is out of scope here. Recorded plainly: its rooted output reaching + the persisted classifier corpus, through SortEmail.cs:597 and QfcFormController.Actions.cs:250, is a + real defect. It is a **separate** defect from #799, it has no acceptance criterion here, and this + item does not fix it. It should be promoted as its own issue together with site 5. +7. **The ToDoModel sort utility.** Left unchanged because it is not a Compile item in its project and + has no live caller. The project is non-SDK-style with explicit Compile Include items and no + globbing, and its entire email-utilities membership is a single unrelated file; the sort file + appears only in a stale .bak project backup that MSBuild never reads. Two searches confirmed this: a + csproj-scoped search for the file name returned no matches, and a repository-wide search for the + entry-point method name found only the source file itself, the .bak backup, and commented-out + references in test files. Converting it would therefore be a no-op unless the file were first added + to the project, which would compile previously-uncompiled code and introduce roughly seven unrelated + compiler problems. That is out of scope. + +### D-B. AC7 rendering branch + +AC7 offers "rendered distinguishably (or filtered)". Neither branch is safe as written, so this +specification settles it. + +**Rejected alternative 1 — the distinguishable branch as stated.** Research verified that no existing +flag or CSS class carries the signal on either surface: the Efc row model has no unresolved flag, the +Efc renderer emits a fixed class string for every suggestion, the Efc stylesheet has no stale rule, the +QuickFiler render DTO exposes eight fields with no flag, and the QuickFiler page CSS has no stale +class. Delivering it therefore costs eight files, including the QuickFiler breadcrumb HTML page +resource and the bridge-messages source. Those two are shared breadcrumb surface that a concurrent +sibling item also edits, so taking this branch would serialise the cohort against a display nicety. + +**Rejected alternative 2 — the naive filter branch.** The unique-suffix resolver returns null for two +different reasons, distinguishable only inside it: zero candidates and multiple candidates. Filtering +every unresolved row would also drop the ambiguous case, in which the folder demonstrably exists — more +than one node matches — and is therefore fileable. That is a silent behaviour regression, not a +display change. + +**DECISION.** Take the "(or filtered)" branch, **restricted to the zero-candidate case only**. A label +for which no snapshot node path ends with the suffix names a folder that no longer exists and cannot be +filed to, so suppressing its row loses nothing. The multiple-candidate case keeps today's rendering +untouched: the row remains present and selectable with the existing fallback. The once-per-label, +per-session logging that AC7 requires unconditionally is implemented for **both** cases, because it +lives in the resolver, above the branch that distinguishes them. + +Placement. The classification (absent versus ambiguous) is produced in the provider, which is the seam +both surfaces route through, so a single change serves both and the log gate and the classification +share one code path. Row suppression is applied where each surface composes its presented row set: the +Efc router file already in the Write Set, and the QuickFiler item-controller files already in the Write +Set. If implementation finds that the QuickFiler drop-down composes its row set only inside the +sibling-owned bridge router, the planner must escalate rather than silently editing that file; the +fallback is to deliver suppression on the Efc surface only, leave the QuickFiler surface at today's +fallback rendering, and record the deviation in Rollout and Follow-up. In every case the AC7 logging +half is delivered on both surfaces. + +Session scope. The provider is created once per form or viewer, not once per process. "Per session" is +therefore realized as "per provider instance", and this specification states that explicitly rather +than leaving it implied. A process-wide static set was rejected because it is mutable global state +shared across viewers and across test methods in one assembly, which the repository unit-test policy +prohibits. QuickFiler viewers are pooled and keep their provider across items, so a pooled viewer keeps +its gate, which is the desired behaviour. The gate must be thread-safe — a concurrent dictionary with +TryAdd, or a HashSet under a lock — because the resolve path is async and its continuations are not +guaranteed to be on one thread. + +### D-C. AC8 requires no code change + +Both render paths were traced end to end. No code alters a leading underscore. + +- The QuickFiler page assigns segment text through the DOM textContent property, which performs no + entity decoding, no escaping and no transformation. +- The Efc page HTML-encodes through a call that encodes ampersand, less-than, greater-than and + double-quote only; underscore and space pass through unchanged and no non-breaking space is emitted. +- The JSON serializer escapes only the double-quote, the backslash and control characters, and the + non-indenting format adds no whitespace inside string values. +- The verbatim splitter splits on path separators with empty entries removed; it inserts nothing and + trims nothing. +- Neither stylesheet contains letter-spacing, word-spacing, text-transform, a first-letter + pseudo-element, or word-break. The QuickFiler segment rule is flex with hidden overflow, ellipsis and + no-wrap; the Efc assets are fifteen rules, none of them a spacing or casing transform. +- A repository-wide search across the six product projects for underscore rewriting and for + letter-spacing, word-spacing, text-transform, first-letter and word-break returned no files. +- The WinForms mnemonic prefix character is the ampersand, not the underscore, so no combo-box or + owner-draw path can be responsible either. + +**Conclusion, stated as a definite finding and not as a probability: the reported space after the +leading underscore was a transcription artifact.** AC8 is satisfied by recording this verified finding. +The renderer is correct and must not be changed. No file is added to the Write Set for AC8. + +### D-D. Sibling separability + +A concurrent sibling item owns the QuickFiler folder drop-down OPEN and CLOSE lifecycle and the +selection-commit ordering. This item owns row TEXT projection and the archive-root stripping contract. + +The AC1 and AC2 chain trim is placed in the hierarchy provider's ancestor-chain method for two reasons, +in this order. First, it is the single seam both surfaces route through: the QuickFiler drop-down +reaches the chain through the UtilitiesCS bridge router's SetSuggestionsAsync and the Efc list through +the QuickFiler router's FetchChainAsync, and both call the provider's GetAncestorChainAsync, so one +change satisfies AC1 on both. Second, it keeps this item's diff off the contended files: trimming +inside SetSuggestionsAsync would require a new field and a constructor parameter on the exact type the +sibling owns. + +Recorded honestly, because it is the one place the two items genuinely overlap: the UtilitiesCS +breadcrumb bridge router file, UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs (489 +lines), contains BOTH concerns. + +- This item's side, the suggestion-decoration members: SetSuggestionsAsync at :29-97, + SetSuggestionFallbacks at :100-119, AddPlainRows at :156-168, CreateFallbackRow at :247-257, and the + single-parameter constructor at :19-23. +- The sibling's side, the lifecycle and selection members: OpenSelector at :185-186, MoveSelector at + :188-189, CommitSelector at :191-192, ActivateSelector at :194-195, ActivateSelectorSubfolder at + :205-208, CancelSelector at :210-211, GetSelectorState at :213, SelectRow at :178-179, SelectItem at + :182-183, Clear at :171-175, Mutate at :239-245, Transition at :267-276, + ReplaceRowsPreservingSession at :478-482, and the selection session field at :14. + +The two sides share the sync lock at :15, the suggestion-generation counter at :16, and the +constructor. A constructor-signature change from this item would collide directly with any sibling edit +to that file. That risk is not softened here; it is the reason for the placement decision above. + +Because the design keeps this item's diff out of that file, it is named in bare prose and is **not** in +the Write Set. A reviewer must verify at review time that the final diff contains no hunk in it, nor in +its search-presentation partial, the breadcrumb selection session or its highlight partial, or the +QuickFiler breadcrumb bridge coordinator or its search partial. Two files that could be mistaken for +the sibling's are in fact this item's Efc-surface files and are in the Write Set: +QuickFiler/Controllers/BreadcrumbBridgeRouter.cs (304 lines). Its Selection partial contains +FetchChainAsync alongside Efc row-selection helpers; the recommended design needs no edit there, and a +reviewer should confirm that. Its Arrows partial is lifecycle-adjacent and is not touched. ## Proposed Fix ### Design summary (what changes where): +Two new pure static helper types are introduced, both in the UtilitiesCS folder namespace, both free of +I/O, COM and logging, and both unit-testable without a provider, a snapshot or Outlook. + +1. **ArchiveStemProjection**, with a single member ToDisplayStem(folderPath, archiveRoot). It returns + the archive-relative stem when the path is strictly under the root, and otherwise returns the input + unchanged. An empty or whitespace root yields the input unchanged, which is what eliminates the AC4 + one-separator strip. It is a separate type rather than a new member on ArchiveStemContract on + purpose: the contract is a hard boundary type that yields an empty string on failure and never + passes its input through, and every display site needs the opposite fallback so an unexpected value + still renders. Adding a lenient overload to the strict contract would blur the invariant #614 + created it for. ToDisplayStem treats "path equals root" as not projectable, because the contract + returns true with an empty stem in that case and an empty display row is worse than the full path; + that reproduces the existing length guard at FolderPredictor.cs:858 exactly. +2. **ArchiveChainProjection**, with a single member TryTrimBelowArchiveRoot(chain, archiveRoot, out + trimmed). It finds the first chain index whose segment path is the archive root itself — detected as + TryMakeArchiveRelative returning true with an empty stem, which is exactly the equality case — and + returns the remainder of the chain after it. It returns false when no such index exists, and also + when that index is the last one, because the leaf is then the root and there is nothing to render + below it. + +The provider's GetAncestorChainAsync applies TryTrimBelowArchiveRoot after the snapshot walk and before +segment mapping, so row order, banner placement and the trash pseudo-row are untouched. On a false +return it logs an error through the log4net ILog already declared in that file and returns an empty +segment list, which routes each surface into its existing fallback. Its GetImmediateSubfoldersAsync is +unaffected: subfolders are below the leaf and therefore below the archive root by construction. + +The provider needs one piece of data it does not have: the archive root string. It is supplied as an +**optional second constructor parameter of delegate type returning a string** — a lazy accessor, not an +eagerly read value, and defaulting to null meaning "no trim". + +- Lazy is mandatory. The archive root property throws InvalidOperationException when the root is + unresolvable, and an existing test at QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242 + exists because of that throw. Reading it eagerly at construction would create a new throw site inside + the Efc controller's breadcrumb configuration and the QuickFiler item controller's pipeline setup, + neither of which is inside a try. +- Optional keeps the roughly twenty existing test constructions in the provider and adapter test files + compiling unchanged, including a construction that passes a single null argument, which binds + unambiguously to the first parameter. + +AC4 and AC5 are then a set of delegations to ArchiveStemProjection: the two duplicated members collapse +to one-line delegations, and the recents append, the recents row mirror and the include-children branch +of the search-subpath helper each gain a projection. + +AC6 projects the score paths inside the Efc router's BindRowsAsync rather than at the controller call +site. The router already computes and normalizes the bound root and already consumes +ArchiveStemContract, and it is 304 lines with room; the controller is 1320 lines, already 2.6 times the +ceiling, so every line added there worsens a standing violation. Placing it in the router also fixes +the join for every caller of the internal overload, not only the one controller. The public +three-argument overload forwards an empty root, so the projection is the identity there and no existing +caller of that overload changes behaviour. Re-keying the join on something other than presented text +was considered and rejected: the builder takes only a string list and a score sequence, so there is no +correlating identity to key on without changing a public signature and every test that calls it. + +AC7 is implemented per decision D-B: the resolver distinguishes the zero-candidate and +multiple-candidate causes, gates its error emission on a thread-safe per-provider set of +already-reported labels, and reports the classification so each surface's row composition can suppress +only the zero-candidate rows. + +AC8 is implemented per decision D-C: no code change; the verified finding is the deliverable. + ### Boundaries and invariants to preserve: +- **AC3, above all.** The filing target and the score-lookup key remain the archive-relative stem. This + is safe by construction: the QuickFiler state row's WithFilingTarget substitutes the presented stem + into the leaf segment's path only, at BreadcrumbStateModel.Row.cs:77-98, and the Efc row's + FilingTarget at BreadcrumbRow.cs:63-65 does the same. Trimming removes leading segments, so the leaf + and therefore the filing value are untouched. A test must pin this rather than leave it to + inspection. +- The archive-stem contract itself is not modified. Its three public members and its semantics are + consumed as they are. +- The snapshot query GetAncestorChain is not modified. It continues to return the full store-rooted + chain; the trim is applied above it. +- The QuickFiler surface's fallback is **not** to be converted to a single segment. AC2's phrase + "existing single-segment fallback" is literally true only on the Efc surface, where the row builder's + empty-chain branch at BreadcrumbRowBuilder.cs:122-131 renders one leaf-only segment. On the + QuickFiler surface the render projection routes non-suggestion rows through the verbatim splitter, so + a fallback for a multi-level stem renders as several segments. That is existing behaviour, it already + satisfies AC2's real requirement that no row shows a mailbox prefix because the verbatim text is the + archive-relative stem, and "fixing" it to one segment would be a new regression against the + search-row rendering that AC1 requires to match. +- Persisted-data round-tripping. Neither wrapper relative-path loader changes, per D-A sites 5 and 6. +- The sibling's files are not touched, per D-D. +- Cancellation and disposal semantics of the provider and both routers are unchanged. + ### Dependencies or blocked work: +None. #614 delivered ArchiveStemContract and #439 delivered the chain rendering this supersedes; both +are present at c431dc32. Live-Outlook confirmation is a human follow-up and does not gate the automated +review. + ### Implementation strategy (what changes, not sequencing): - + #### Files/modules to change: +See the Write Set section. It is the authoritative list. + +**Ordering constraint that the planner must honour.** +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is **exactly 500 lines** — at the repository +ceiling, not near it. The provider construction at :147-149 must gain one argument, and CSharpier will +format the result as an additional line because the collapsed single-line call is about 126 columns +including indent, well past the print width. Adding that argument first would push the file to 501 +lines and break the ceiling. Therefore the breadcrumb pipeline helper EnsureBreadcrumbPipeline +(:132-163, 32 lines including its comment and its ExcludeFromCodeCoverage attribute) must be relocated +into the new partial `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` **before** the +provider construction gains its argument, leaving the viewer-setup file at roughly 468 lines. That +relocation is the only reason both files appear in the Write Set. + #### Functions/classes/CLI commands impacted: +New: ArchiveStemProjection.ToDisplayStem; ArchiveChainProjection.TryTrimBelowArchiveRoot. Modified: +OutlookFolderHierarchyProvider's constructor, GetAncestorChainAsync and ResolveByUniqueSuffix (the last +becomes an instance member so it can reach the per-instance gate); FolderPredictor's +ProjectSuggestionPath, AddRecents, AddRecentRows and GetOlSubpath; QfcItemController's +ProjectPredeterminedFolder and EnsureBreadcrumbPipeline (relocated); the Efc router's BindRowsAsync; +the Efc controller's provider construction. No CLI surface exists in this component. + #### Data flow and validation changes: +Display text only. The trim is applied to segment lists after chain resolution; the display projection +is applied to suggestion, recents and score path strings before they are presented or joined. The +filing target and the score-lookup key are unchanged in value, and the score join changes only in that +both sides of it are now projected against the same root. No persisted data, schema or file format +changes. + #### Error handling and logging updates: +- A chain that does not pass through the archive root is logged at ERROR by the provider, through the + ILog already declared in that file, and yields an empty segment list so each surface uses its + existing fallback. +- The stale-label ERROR emission in the resolver is gated to once per distinct label per provider + instance, using a thread-safe set. Its message keeps both existing causes distinguishable. +- No new logger shape is introduced anywhere. Tests observe the gate through an injected delegate sink + rather than a log4net appender, so no test mutates the process-global logger repository. + #### Rollback/feature-flag considerations (if applicable): +No feature flag. The optional constructor parameter is itself the effective off switch: constructed +without a root accessor, the provider behaves exactly as it does today. Rollback is a revert of the +branch, with no data or configuration migration. + ### Technical specifications (interfaces/contracts): #### Inputs/outputs and formats: +The shared projection inherits its semantics from ArchiveStemContract.TryMakeArchiveRelative, verified +at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:106-145: + +- **Prefix-anchored.** The test is StartsWith at :131, never Contains and never Replace. +- **Ordinal, case-insensitive**, on both the equality test at :124 and the prefix test at :131. +- **Separator-terminated.** The character at the root's length must be a backslash or a forward slash, + checked at :137-141; otherwise the result is false. +- **Trailing separators on the root are ignored**, trimmed at :118 for both separator characters. A + root that is nothing but separators trims to length zero and is treated as empty. +- **Returns false with an empty stem rather than passing the input through**, at :112 and :129-141. + Null, empty or whitespace input on either parameter returns false with an empty stem, at :113-122. +- **Returns true with an empty stem when the path equals the root**, at :124-127. +- The returned stem never leads with a separator; both separator characters are trimmed from its start + at :143. +- **The Archive2 false-prefix boundary case.** A path under a sibling folder named Archive2, tested + against a root ending in Archive, yields the character 2 at the root's length. That is not a + separator, so the contract returns false and the path is not mis-stripped. The current + ProjectSuggestionPath handles this case only by accident, because its concatenated prefix happens to + carry the trailing separator; the contract handles it by rule. This case must appear in the tests for + both new helpers. + +ArchiveStemProjection.ToDisplayStem returns the stem when TryMakeArchiveRelative returns true **and** +the stem is non-empty, and returns the input unchanged in every other case, including the equal-to-root +case and the empty-root case. + +ArchiveChainProjection.TryTrimBelowArchiveRoot returns true and the segments after the archive-root +node when such a node exists and is not the last element; it returns false and leaves the output empty +otherwise. + #### Required configuration keys and defaults: +None. No new setting, no settings-designer change, and no user-facing configuration surface. The +archive root continues to come from the existing application globals accessor. + #### Backward-compatibility expectations: +Additive at the type level: two new public static types, one new optional constructor parameter on the +provider, and one added field on no persisted contract. Existing callers compile unchanged. The +behavioural changes that are intended and not backward compatible are the superseded #439 lineage +rendering and the removal of the empty-root one-separator strip that AC4 names. + #### Performance constraints (latency/throughput/memory): +The trim is a single linear scan of a chain whose length is the folder depth, executed once per +resolved row. The display projection is one StartsWith and at most one Substring per string. Neither +introduces allocation per call beyond the produced string, and the contract performs no per-call regex +allocation. No performance threshold is asserted as an acceptance criterion, because no measured +baseline exists for breadcrumb binding. + +## Write Set + +Every path below is a concrete repository-relative path this change creates or modifies, written with +forward slashes and marked CREATE or MODIFY. This section is the change footprint; the planner reads +it. Any file the implementation must touch that is not listed here is a scope deviation and must be +recorded as one in Rollout and Follow-up rather than added silently. + +### Production sources + +- `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` — CREATE. The shared display projection. +- `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` — CREATE. The chain trim. +- `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` — MODIFY. 141 lines, ample + room. Chain trim, optional root accessor, AC2 error, AC7 gate and classification. +- `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` — MODIFY. 1003 lines, already over the ceiling; + the change is net line-neutral or negative because the thirteen-line stripping body collapses to a + delegation. +- `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` — MODIFY. 313 lines. AC4 delegation and + the QuickFiler-surface zero-candidate suppression. +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` — MODIFY. 304 lines. AC6 score projection and the + Efc-surface zero-candidate suppression. +- `QuickFiler/Controllers/EfcFormController.cs` — MODIFY. 1320 lines. One argument added at the provider + construction near line 1053. +- `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` — CREATE. Receives the relocated + breadcrumb pipeline helper. +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` — MODIFY. Exactly 500 lines today; see the + ordering constraint in Implementation strategy. + +### Test sources + +- `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` — CREATE. +- `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` — CREATE. +- `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` — CREATE. A new + file rather than growing the existing provider test file. +- `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` — CREATE. AC5. +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` — CREATE. AC6. +- `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` — MODIFY, retarget. + GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments pins the superseded #439 root-to-leaf + chain. It would remain green untouched only because it constructs the provider without a root + accessor, which is not how production constructs it; leaving it that way would pin only the disabled + configuration. +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` — MODIFY, retarget. Its chain + fixtures build a segment for the Archive root node and assert it renders. +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` — MODIFY, retarget if + its fixtures also encode the rooted chain. Listed so a required change is not a scope violation; if + it proves unaffected the diff simply contains no hunk for it. +- `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` — MODIFY, retarget. Its + assertion at :220-223 pins the empty-root one-separator strip that AC4 eliminates. It must be updated + to assert the new behaviour, not preserved. + +### Project files + +All four are legacy non-SDK projects with explicit Compile Include items and no globbing, so every new +.cs file needs a one-line self-closing entry. + +- `UtilitiesCS/UtilitiesCS.csproj` — MODIFY. Entries for the two new production helpers. +- `QuickFiler/QuickFiler.csproj` — MODIFY. Entry for the new item-controller partial. +- `UtilitiesCS.Test/UtilitiesCS.Test.csproj` — MODIFY. Entries for the four new UtilitiesCS test files. +- `QuickFiler.Test/QuickFiler.Test.csproj` — MODIFY. Entry for the new QuickFiler test file. + +### The two space-containing paths + +Research concluded that neither is to be modified, so neither belongs in the Write Set and neither +carries backticks. Both are restated in words so a path extractor cannot misread them as write targets: + +- **The ToDoModel email-utilities sort file** — directory ToDoModel, then a directory whose name is the + two words Email and Utilities separated by one space, then SortItemsToExistingFolder.cs. Not + modified: it is not a Compile item in its project and has no live caller, so converting it would be a + no-op unless the file were first added to the build, which would compile previously-uncompiled code + and is out of scope. See decision D-A, site 7. +- **The folder wrapper source file** — directory UtilitiesCS, then OutlookObjects, then Folder, then a + file named FolderWrapper followed by one space and then the .cs extension. Not modified: it is 532 + lines and already over the 500-line ceiling, and its full-path fallback is depended upon by the + persisted relative-path restore and by the classifier corpus. See decision D-A, site 6. + +Because both are excluded, **this item's footprint contains no space-containing path.** That matters +mechanically as well as for scope: the token extractor splits every backtick span on whitespace, so a +backticked space-containing path could never be harvested as one token in any case. + +Also not modified, and written without backticks for the same reason: +UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs — see decision D-A, site 5. Note the absence +of backticks on that line is deliberate and must not be "corrected". + ## Assumptions, Constraints, Dependencies -- Assumptions (environment, data, access): -- Constraints (budget, performance, compatibility): -- External dependencies (services, libraries, releases): + +- Assumptions: the archive root resolves successfully in the running add-in, and throws rather than + returning null when it does not, which is why the accessor is lazy; persisted suggestion labels are + archive-relative when written by the current code paths; the snapshot contains the archive-root node + whenever it contains any node below it. +- Constraints: .NET Framework 4.8 and legacy non-SDK projects, so every new file needs an explicit + Compile entry; the 500-line file limit, with the viewer-setup file exactly at it and three other + files already over it; no temporary files, no wall-clock waits and no mutable global state in tests; + the provider is constructed on the UI thread inside methods that are not wrapped in a try. +- External dependencies: MSTest, Moq, FluentAssertions and log4net, all already referenced. No new + package. ## Data / API / Config Impact -- User-facing or API changes: -- Data or migration considerations: -- Logging/telemetry updates (if any): -- Compatibility notes (CLI flags, config schemas, versioning): + +- User-facing changes: suggestion rows in both QuickFiler modes and in the Efc view no longer show the + mailbox and Archive segments; recent-folder rows are projected the same way as suggestions; an + archive-rooted suggestion on the Efc surface now shows its percentage; a persisted label naming a + folder that no longer exists is suppressed on the zero-candidate case per decision D-B. +- API changes: two new public static helper types; one new optional constructor parameter on the + hierarchy provider; one resolver member changes from private static to a private instance member. No + public interface signature changes. +- Superseded criteria recorded for the reviewer: #439's full root-to-leaf ancestor lineage is + superseded by AC1 and AC2 of this specification. #439's filing-target and score-key constraint is + preserved and is carried forward as AC3. +- Data or migration considerations: none. No persisted label is rewritten, and the two wrapper loaders + that feed persisted data are deliberately unchanged. +- Logging/telemetry updates: one new ERROR for a chain that misses the archive root; the existing + stale-label ERROR gated to once per distinct label per provider instance. No new logger shape. +- Compatibility notes: no CLI flags, no config schema, no settings surface. New files require explicit + Compile Include entries in four legacy projects. ## Test Strategy -Seeded from issue: -Acceptance criteria settled with the maintainer on 2026-09-06: +MSTest with Moq and FluentAssertions, per the C# unit test policy. No temporary files, no Thread.Sleep, +no Task.Delay, no wall-clock waits, no live Outlook COM, no WebView2, and no mutation of the +process-global log4net repository. + +Validation notes carried forward from the issue: + +- Unit coverage areas: chain trimming below the archive root (chain through root, chain not through + root, chain equal to root); the shared projection against the #614 contract cases (case-insensitive, + trailing separators, the Archive2 boundary); recents projection; the Efc score join with rooted and + relative paths; renderer segment text preservation for leading underscores. +- Integration scenario to retest: bind a row set containing a suggestion, a search result, a banner + row, the trash pseudo-row and a stale label; assert lineage on both folder row kinds, fallback on the + stale label, and no lineage on the banner or trash rows. This is drivable entirely through the row + builder and the render projection, with no WebView2 and no Outlook. +- Manual verification notes: QuickFiler ordinary and High Confidence, plus the Efc view. Confirm no row + begins with the mailbox or Archive, and that clicking a middle segment navigates to that ancestor. + +New unit tests: + +- ArchiveStemProjection: table tests over the #614 boundary cases — under root, equal to root, Archive2, + a root supplied with trailing separators, an empty and a whitespace root, null and empty path, + forward-slash separators, and mixed case. +- ArchiveChainProjection: chain through the root; chain not through the root; chain whose leaf is the + root; empty chain; single-element chain; root supplied with a trailing separator. All constructible + from segment literals with no snapshot and no COM. +- Provider trim tests: a mocked tree service returning a hand-built snapshot, plus a root accessor. + Assert the trimmed chain, assert the AC2 error and empty result for a chain that misses the root, and + assert the AC7 gate by resolving the same unresolvable label twice and counting emissions through an + injected delegate sink. +- Recents projection: exercise both the string array and the row array on a predictor whose recents + list holds one rooted and one relative entry, and assert text parity between the two, which is the + documented contract at FolderPredictor.cs:233-242 and is currently unasserted. +- Efc score join: a rooted-score and relative-row pair asserting a non-empty percentage cell. The + existing #614 router fixtures already build rooted and relative target pairs and are the model. +- AC3 pin: after trimming, assert the filing target and the score-lookup key are still the + archive-relative stem. This must be its own assertion, not an incidental consequence. + +Retargeting obligations. These encode behaviour this specification supersedes; retarget, do not delete, +and do not merely add new tests alongside them: + +- The provider chain test GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments in + `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` — rewrite so the + chain assertion is made against a provider configured with a root accessor, as production configures + it. A companion case may keep the untrimmed expectation for the no-accessor construction, but the + production configuration must be the one that is pinned. +- The #439 Efc router tests in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` + and, if affected, `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` — + their fixtures build an Archive-root segment and assert it renders. +- The empty-root assertion at :220-223 in + `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` — it pins the + one-separator strip AC4 eliminates. + +Fail-before evidence is required for at least one AC1 test, the AC4 empty-root behaviour change, and +the AC6 score-join test, recorded under this feature folder's evidence/regression-testing directory. +Baseline and final QA gate notes are recorded under this feature folder's evidence/baseline and +evidence/qa-gates directories. No evidence artifact is written to any other location. + +Coverage: changed lines must not regress, and the two new helper types target at least 90 percent per +the repository unit-test policy. The repository-wide figure is reported against the testable +denominator per the CLAUDE.md UT2 exemptions and must not decrease. + +Toolchain commands to run, in this order, restarting from the first on any failure or auto-fix: +1. `dotnet tool run csharpier format .` then `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 over the UtilitiesCS.Test and QuickFiler.Test assemblies and their dependents, + with coverage collection. + +Manual validation: performed by a human against a live Outlook profile after the fix is built, per the +manual verification notes above. It does not gate the automated review. + +## Acceptance Criteria - [ ] AC1: Suggestion rows and search-result rows in both the QuickFiler item view and the Efc view render the lineage starting at the first segment below the archive root, with the same arrow rendering and clickable ancestor segments for both row kinds. Example: `_Active Projects -> Build RGF Org and Team -> Sales Lead`. - [ ] AC2: A resolved chain that does not pass through the archive root node is logged as an error and rendered with the existing single-segment fallback; no row ever shows the mailbox or Archive segment. @@ -136,36 +840,62 @@ Acceptance criteria settled with the maintainer on 2026-09-06: - [ ] AC7: Persisted suggestion labels that fail hierarchy resolution are rendered distinguishably (or filtered) and logged once per label per session, not once per render. - [ ] AC8: The leading-underscore rendering question (`_Active Projects` vs `_ Active Projects`) is verified and, if the renderer alters it, corrected. -Validation: - -- [ ] Unit coverage areas: chain trimming below the archive root (chain through root, chain not through root, chain equal to root); shared projection against the #614 contract cases (case-insensitive, trailing separators, `Archive2` boundary); recents projection; Efc score join with rooted and relative paths; renderer segment text preservation for leading underscores. -- [ ] Integration scenario to retest: bind a row set with a suggestion, a search result, a `====` banner, the trash pseudo-row, and a stale label; assert lineage on both folder row kinds, fallback on the stale label, none on banner/trash. -- [ ] Manual verification notes: QuickFiler ordinary and High Confidence, plus Efc view: confirm no row begins with the mailbox or Archive, and that clicking a middle segment navigates to that ancestor. - -- Regression tests to add or update: -- Unit tests (pytest) for the fixed behavior and boundaries: -- Edge cases and negative scenarios (invalid inputs, missing data, boundary values): -- Error handling and logging verification: -- Coverage impact and targets for changed lines/modules: -- Toolchain commands to run (format → lint → type-check → test): -- Manual validation steps (if required): - - -## Acceptance Criteria -- [ ] Repro steps now produce the expected behavior in all documented environments. -- [ ] Regression test(s) added and passing (list file path and test name). -- [ ] Edge cases and invalid inputs are handled with correct errors or fallbacks. -- [ ] No unintended behavior changes outside the defined scope. -- [ ] Required logs/telemetry updated and validated (if applicable). -- [ ] Performance constraints met or explicitly waived with rationale. -- [ ] Full toolchain pass completed (format → lint → type-check → test). -- [ ] Docs/config references updated to match the new behavior. +Notes for the reviewer, which do not add or weaken any criterion: +- AC4's site disposition is settled by decision D-A. Four sites convert, three are deliberately left + with reasons recorded. +- AC7 is satisfied through the filtered branch restricted to the zero-candidate case, per decision D-B. + The logging half applies to both causes and both surfaces. +- AC8 is satisfied by the verified finding in decision D-C. The renderer is correct; no code change is + the correct outcome, and a renderer change would be a defect. ## Risks & Mitigations -- Technical or operational risks: -- Mitigations and rollbacks: + +- **Cohort contention.** Three sibling items run concurrently, and one of them owns the QuickFiler + drop-down lifecycle in a file that also contains this item's suggestion-decoration members. + Mitigation: the AC1 and AC2 trim is placed in the hierarchy provider, the single seam both surfaces + route through, so this item's diff stays entirely off that file and the five other sibling-owned + files; AC7's distinguishable branch, which would have pulled in the shared QuickFiler page resource + and the bridge-messages source, is deliberately not taken; and every path in this document that the + change does not write is unbackticked so the harvested blast radius matches the real footprint. + Verification: the reviewer confirms the final diff contains no hunk in any file named in decision + D-D. +- **Persisted-corpus risk.** Converting either wrapper relative-path loader to the strict contract would + make it yield an empty string where it now yields a full path, which would change the values written + to the persisted classifier corpus and would silently break the restore branch that exists to recover + a rooted stored value. Mitigation: both sites are left unchanged by decision D-A, the reason is + recorded rather than left implicit, and the underlying defect is promoted as a separate issue instead + of being folded in here. +- **500-line ceiling risk.** The viewer-setup file is exactly at the ceiling, so the single argument + this change adds to the provider construction would break it. Mitigation: the ordering constraint in + Implementation strategy requires the pipeline helper to be relocated into the new partial first. Two + further files in the Write Set are already over the ceiling; the change to each is a delegation or a + single argument and must be net line-neutral or negative, and the two new helpers are new files + rather than additions to the predictor. +- **Retargeting risk.** A retargeted test can be rewritten in a way that no longer pins anything. + Mitigation: each retargeting obligation names the specific assertion and the specific configuration + the rewritten test must exercise, and the AC3 pin is required as its own assertion. +- **Under-reported site risk.** AC5 names only the recents append, but the row mirror is what the + breadcrumb consumes. Mitigation: decision D-A converts both and a test asserts text parity between + the string array and the row array. +- Rollback: revert the branch. The optional constructor parameter means an intermediate state with the + accessor unsupplied behaves exactly as today. ## Rollout & Follow-up -- Release/rollout steps: -- Post-fix monitoring or clean-up tasks: -- Links: issue, PRs, related docs + +- Release/rollout steps: merge to main after review; the add-in is picked up by rebuilding the + registered checkout, with no re-registration step. +- Post-fix manual verification: a human confirms in QuickFiler ordinary and High Confidence modes and + in the Efc view that no row begins with the mailbox or Archive segment, that a middle segment click + navigates to that ancestor, that filing still lands in the correct folder, and that an archive-rooted + suggestion in the Efc view shows its percentage. Recorded as an evidence note under this feature + folder's evidence directory. It does not gate the automated review. +- Post-fix monitoring: confirm the stale-label error appears once per distinct label per viewer rather + than once per render, and that no new chain-misses-archive-root error appears in normal operation. +- Follow-up issues to open: (1) the two wrapper relative-path loaders, whose unanchored, case-sensitive + Replace with a full-path fallback lets a rooted label enter the persisted classifier corpus — a real + defect, deliberately out of scope here per decision D-A sites 5 and 6; (2) optionally, the unanchored + archive-root comparison in the mail-item loading helper's ResolveFolderRoot, recorded during research + as a root selector rather than a stripper. +- Links: issue https://github.com/drmoisan/TaskMaster/issues/799; the research record in this feature + folder under research/; superseded predecessor #439; the archive-stem contract predecessor #614; the + duplication predecessor #678. From b9692885eb17422be4cbe88f5fe14872aa5edcbd Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sun, 6 Sep 2026 23:01:20 -0400 Subject: [PATCH 03/13] docs(799): author the atomic implementation plan Four phases and 74 tasks covering worktree bootstrap, fail-before regression tests, the implementation, and the final quality-control loop. The plan validator passes; its single warning is a false positive raised by the plan's own delta self-check prose, which names the git diff rule it satisfies while every real invocation is anchored to the recorded base commit. Three planner determinations depart from the spec's literal wording and are recorded with their evidence: the acceptance-criterion 7 row suppression takes the spec's documented escalation to the Efc surface because the QuickFiler row set is composed only inside a file owned by a concurrent sibling item; the acceptance-criterion 6 score projection is additive rather than substitutive so a rooted presented row keeps its percentage; and the two issue 439 router test files need no change because their provider is a strict mock above the trimming seam. Co-Authored-By: Claude Fable 5.1 --- .../plan.2026-09-06T22-01.md | 867 +++++++++++++++++- 1 file changed, 840 insertions(+), 27 deletions(-) diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md index 902da72cd..28ffdcc8b 100644 --- a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md @@ -4,41 +4,854 @@ - **Parent (optional):** none - **Owner:** drmoisan - **Last Updated:** 2026-09-06T22-01 -- **Status:** Draft -- **Version:** 0.1 +- **Status:** Ready for preflight +- **Version:** 1.0 +- **Work Mode:** full-bug (resolved from `issue.md` line 12 and `spec.md` line 9) +- **Language in scope:** C# only (UtilitiesCS, QuickFiler, UtilitiesCS.Test, QuickFiler.Test; four legacy non-SDK projects with explicit Compile Include items and no globbing) +- **Authoritative AC source:** `spec.md`, section "Acceptance Criteria", lines 834-841, AC1 through AC8. There is no user-story.md in this feature folder and none is required. -**Fail-closed evidence rule:** Include explicit baseline artifact tasks, final-QA artifact tasks, and coverage-comparison tasks for each in-scope language when policy requires coverage. If any required baseline artifact, QA artifact, or coverage-comparison artifact is missing, the audit verdict must be BLOCKED or INCOMPLETE, never PASS. +**Fail-closed evidence rule:** Every baseline, regression, and QA artifact named by a task must exist with all required fields before that task may be checked off. A missing or field-incomplete artifact makes the outcome BLOCKED or INCOMPLETE, never PASS. -**Evidence accounting rule:** Record the expected artifact path or location in each evidence-producing task. Do not mark evidence-backed work complete without the artifact. +**Evidence accounting rule:** Each evidence-producing task names its exact artifact path. Work is not complete without the artifact. +--- -**Phase 0 — Context & Inputs** -- [ ] [P0-T1] Link approved spec: -- [ ] [P0-T2] Record branch/commit baseline: -- [ ] [P0-T3] List required environment/fixtures/data: +## Plan-wide rules -**Phase 1 — Preparation** -- [ ] [P1-T1] Confirm scope is locked for this fix (no open spec gaps) -- [ ] [P1-T2] Sync workspace to target branch and ensure tooling is available +**R1 — Evidence location (non-overridable).** Every evidence artifact is written under the canonical scheme +`/evidence//` with `` in `baseline`, `regression-testing`, `qa-gates`, `issue-updates`, `other`, where +`` abbreviates the repository-relative feature folder +docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799. +No caller supplied a non-canonical evidence path, so no `EVIDENCE_LOCATION_OVERRIDE_REJECTED` record is required by this plan. +The Cobertura document written to artifacts\csharp\coverage.xml is a tool output document, not an evidence artifact: +.claude/hooks/enforce-evidence-locations.ps1 names artifacts/csharp/ as an explicitly permitted path at its line 26 and does not +list it among the forbidden prefixes at lines 64-77. -**Phase 2 — Regression Test (must fail first)** -- [ ] [P2-T1] [expect-fail] Add a small, deterministic regression test in the standard module file (use `tests/bugs//#799-.py` only if no clear home exists) -- [ ] [P2-T2] [expect-fail] Run the regression to confirm it fails and captures the repro +**R2 — Evidence artifact schema.** Every command-bearing task writes an artifact containing, at minimum, the literal field +lines `Timestamp:` (format `yyyy-MM-ddTHH-mm`), `Command:`, `EXIT_CODE:`, and `Output Summary:`. A task whose command is +expected to exit non-zero additionally writes `ExpectedExitCode: 1`. -**Phase 3 — Minimal Fix** -- [ ] [P3-T1] Apply the smallest change needed to make the regression test pass; avoid opportunistic refactors +**R3 — Evidence filename length and path hygiene.** This feature folder name is 84 characters, so every evidence FILENAME is +kept short (the form `p0-t2-base.md`). No artifact may contain an absolute host path or a host account name: replace a +repository root with ``, a user-profile segment with ``, and a machine name with ``. This applies to +tool stdout, MSBuild logs, stack traces, Cobertura `filename` values, and TRX content alike. TRX files carry `runUser` and +`computerName` attributes in mixed casing; never paste raw TRX content into an artifact, and record only parsed counter +values and fully qualified test names. The one deliberate exception is the vswhere-resolved vstest.console.exe path that +[P0-T7] is required to record, because pinning that path is the task's whole purpose. -**Phase 4 — Verification Loop** -- [ ] [P4-T1] Re-run repro and regression test to confirm expected behavior -- [ ] [P4-T2] Run formatter → linter → type checker → tests; restart loop if any step changes files or fails -- [ ] [P4-T3] Record baseline, post-change, and comparison artifact paths for each in-scope language where coverage is required +**R4 — Token-assertion case rule.** Every token-presence or token-absence assertion in this plan is case-sensitive. +Use `Select-String -CaseSensitive -SimpleMatch` or `git grep` without `-i`. PowerShell `-match` and a bare `Select-String` +are case-insensitive and must not be used for these gates. -**Phase 5 — Documentation & Status** -- [ ] [P5-T1] Update spec/issue with outcomes, decisions, and any deviations from scope +**R5 — Named tests before phrase searches.** Where an acceptance condition can be carried by a named MSTest method, the +condition is stated as that method passing. Phrase searches are used only where no test can express the condition, and +every such literal is quoted verbatim in this document outside its command span. -**Phase 6 — PR & Handoff** -- [ ] [P6-T1] Prepare PR notes (summary, risks, validation performed, links to tests) and request review +**R6 — Base reference.** [P0-T2] records `BASE-SHA` (the commit at plan start) into `/evidence/baseline/p0-t2-base.md`. +Every later `git diff` in this plan uses that recorded value as its ref operand. No SHA is pinned as a literal expectation +in this document. -**Phase 7 — Rollout / Follow-up** -- [ ] [P7-T1] Capture deployment/rollout notes and post-fix monitoring items -- [ ] [P7-T2] Record links (issue, PRs, related docs) for traceability +**R7 — Scope pathspec.** The spec's Write Set is the change footprint, but this plan is additionally required to write +evidence artifacts under `/evidence/` and to check off AC boxes in `spec.md`. Every scope-boundary gate in this plan +is therefore evaluated over the source pathspec `'*.cs' '*.csproj'` only, carries a `git add --intent-to-add` companion so +newly created files are visible to an anchored diff, and carries a `git status --porcelain --untracked-files=all` companion +because neither mechanism alone is correct in both states: an anchored diff cannot see an untracked file, and porcelain +status goes empty once the change is committed. + +**R8 — 500-line ceiling, `.cs` only, with three disclosed pre-existing violations.** +.claude/rules/general-code-change.md caps production code, test code and reusable script files at 500 lines. It does not +reach project files: `.csharpierignore` lines 9-14 record that project files are owned by Visual Studio and are not C# +source. Every ceiling assertion in this plan is therefore scoped to `.cs` paths, and every project-file count is recorded as +an exempt observation rather than asserted. + +Three files this plan touches are ALREADY over the ceiling before any change, measured in this worktree in this pass: +`UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` 1003 lines and `QuickFiler/Controllers/EfcFormController.cs` 1320 +lines are in the Write Set; UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs is 1066 lines and is NOT touched +by this plan. These are disclosed as pre-existing, not repaired here, and are gated by a per-file budget rather than by the +ceiling — see D11. + +**R9 — Hard ordering constraint at the ceiling.** `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is EXACTLY 500 +lines today, at the ceiling and not near it. Its provider construction at lines 147-149 must gain one argument. The +compensating relocation of the breadcrumb pipeline helper into the new partial +`QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` is therefore sequenced BEFORE the argument is added, by +[P2-T3] preceding [P2-T14]. Any other order puts the file at 501 lines in an intermediate state. The count is re-measured +after the final CSharpier pass by [P3-T10], because the formatter can change line counts. + +**R10 — MSBuild command forms.** The two gate builds use exactly the CLAUDE.md commands, with `/t:Rebuild` and without +`/p:Nullable=enable`. Iterative builds inside Phases 1 and 2 use `/t:Build` with no `/p:` gate switches; those builds exist +to produce test assemblies, not to run gates, and every source edit changes a timestamp so `CoreCompile` is not skipped. +A project-file build, if ever needed, must use `/p:Platform=AnyCPU`; the quoted `"/p:Platform=Any CPU"` form is a +solution-level alias only. + +**R11 — Shell-variable re-binding (non-optional).** No variable survives between tasks: every command block runs in its own +shell. A block that uses `$vstest` must be preceded, in that same block, by the two resolution lines that [P0-T7] pins. A +block that uses `$BaseSha` must be preceded, in that same block, by a binding that resolves to the 40-hexadecimal value +[P0-T2] recorded as `BASE-SHA`, with no placeholder token left in the command. An unbound `$BaseSha` degrades an anchored +`git diff --name-only` into the ref-less form, which compares the worktree against the index and passes vacuously once the +change is committed, so the binding is load-bearing rather than cosmetic. A block that runs a `dotnet` command must first +re-bind `DOTNET_ROOT` and `PATH` to the repository-local SDK, because this worktree has no host SDK that satisfies +`global.json`. The three preambles are: + +```powershell +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +$vswhere = "${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 +$BaseSha = (Select-String -Path 'docs\features\active\2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799\evidence\baseline\p0-t2-base.md' -CaseSensitive -Pattern '^BASE-SHA: ([0-9a-f]{40})$').Matches[0].Groups[1].Value +``` + +The `$BaseSha` binding reads the recorded value out of the [P0-T2] artifact rather than carrying a hand-typed literal. That +substitutes the recorded value exactly, leaves no placeholder in the plan text, and fails loudly if [P0-T2] never ran or +recorded a malformed value. An executor that prefers to paste the recorded 40-hexadecimal value directly after [P0-T2] has +run satisfies this rule equally. Each affected task's `Output Summary:` records the resolved vstest path reduced per R3 and +the `$BaseSha` value it bound. + +**R12 — Path-notation rule for this document.** Backticks in this plan are reserved for (a) the concrete repository paths +this change creates or modifies, taken from the `spec.md` Write Set and written with forward slashes; (b) bare filenames +carrying no directory separator, which a path extractor cannot classify as repository paths; and (c) code identifiers. +Every other file reference — comparisons, precedents, citations, sibling-owned files and out-of-scope files — is written as +bare prose with no backticks, including its File.cs:123 line citation. Inside fenced command blocks, Windows backslash path +forms are used wherever a forward-slash form is not required by git, so those spans are inert to the extractor. This is a +scheduling requirement, not a style preference: Get-BlastRadius harvests backtick-delimited path tokens from both the plan +and the spec and treats every accepted token as a write claim, with no notion of polarity, so a backticked path inside a +sentence saying the change will not touch it would still serialize this item against a concurrent sibling. + +**R13 — Suite selection excludes the environmentally-hanging shell-icon classes.** Four UtilitiesCS.Test classes that call +SHGetFileInfo stall vstest on this machine. Every multi-assembly run in this plan carries the same four +`FullyQualifiedName!~` clauses that the most recent completed C# bug plan used, and the exclusion is recorded in each +artifact so the reduced denominator is visible on both sides of every comparison. + +--- + +## Decisions Record + +**D1 — The AC1/AC2 trim is placed in the hierarchy provider, and that keeps this item's diff off all six sibling-owned +files.** The QuickFiler drop-down reaches the ancestor chain through the UtilitiesCS breadcrumb bridge router's +SetSuggestionsAsync, and the Efc list through the QuickFiler router's FetchChainAsync at +QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs:50-81; both call the provider's GetAncestorChainAsync, so one +change satisfies AC1 on both surfaces. The six files a concurrent sibling owns are, in bare prose: +UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs (489 lines), +UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.SearchPresentation.cs, +UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs, +UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.Highlight.cs, +QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs, and QuickFiler/Viewers/BreadcrumbBridgeCoordinator.Search.cs. +[P3-T11] asserts the final anchored diff contains no hunk in any of them. + +**D2 — The archive root reaches the provider as an OPTIONAL SECOND CONSTRUCTOR PARAMETER of delegate type returning a +string, never as an eagerly read value.** IOlObjects.ArchiveRootPath throws InvalidOperationException when the root is +unresolvable, and QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242 declares +BindBreadcrumbRowsAsync_WhenArchiveRootThrows_ReportsOnceAndDoesNotThrow precisely because of that throw. Reading the root +eagerly at construction would create a new throw site inside EfcFormController.ConfigureBreadcrumbControl (which this plan +edits at line 1053) and inside the relocated breadcrumb pipeline helper, neither of which is inside a try. Optional with a +null default keeps every existing provider construction compiling unchanged: 13 constructions in +UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs (lines 76, 98, 118, 139, 157, 175, 193, 218, +240, 269, 299, 316, 338) and 6 in UtilitiesCS.Test/OutlookObjects/Folder/FolderHierarchyProviderAdapterTests.cs (lines 98, +122, 142, 170, 185, 202), including the single-null-argument construction at OutlookFolderHierarchyProviderTests.cs:316, +which binds unambiguously to the first parameter because the type declares exactly one constructor. + +**D3 — The accessor is stored in an internal get-only auto-property, not a private readonly field.** [P1-T4] adds the +parameter before [P2-T5] reads it. A `private readonly` field assigned and never read raises CS0414, which +`/p:TreatWarningsAsErrors=true` promotes to an error. An internal get-only auto-property has a compiler-generated backing +field read by its getter and raises no such warning, so the Phase 1 intermediate state is warning-clean. `internal` is +sufficient because only the declaring type reads it, and UtilitiesCS grants InternalsVisibleTo("UtilitiesCS.Test") at +UtilitiesCS/Properties/AssemblyInfo.cs:19. + +**D4 — The AC7 absence classification is published through a NEW SMALL PUBLIC INTERFACE declared in the provider's own +file, not by adding a member to IFolderHierarchyProvider.** UtilitiesCS/OutlookObjects/Folder/IFolderHierarchyProvider.cs +declares exactly three members at lines 31-63. net48 has no default interface members, so a fourth member would break every +implementer, and — decisively — every breadcrumb router test constructs `new Mock(MockBehavior.Strict)`, +so a strict mock would throw the first time production called the new member. Declaring +`IFolderLabelAbsenceReport` in `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` and having the Efc +router obtain it with an `as` cast in its existing constructor body means a strict mock simply is not an +`IFolderLabelAbsenceReport`, the field is null, and AC7 suppression is inert in every existing router test. Production is +unaffected: research and this pass both confirm no adapter wraps the provider — the two production constructions at +QuickFiler/Controllers/EfcFormController.cs:1053 and `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs`:147 hand the +concrete provider straight to the router. The interface is public because QuickFiler consumes it; it lives in the +provider's file because it is a diagnostics view of exactly that type, and no new UtilitiesCS production file is in the +Write Set. + +**D5 — ESCALATION BRANCH TAKEN: AC7 row suppression is delivered on the Efc surface only.** `spec.md` decision D-B requires +the planner to escalate rather than edit a sibling-owned file if the QuickFiler presented row set proves to be composed only +inside the sibling-owned bridge router. Re-derived in this pass: it is. The QuickFiler drop-down's row set is built as the +local `built` list inside FolderBreadcrumbBridgeRouter.SetSuggestionsAsync at +UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs:42-86, and swapped into the model under the shared lock at +:88-96. `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` only hands the predictor's row model to the viewer at +its lines 212 and 221; at that moment no provider resolution has been attempted, so the zero-candidate classification does +not yet exist there and could be produced only by adding a second, synchronous resolution pass on the UI thread inside +AssignFolderComboBox — which is both a duplicate of the router's work and a change to the very ordering the sibling owns. +The documented fallback is therefore taken: [P2-T12] delivers suppression on the Efc surface only, the QuickFiler surface +keeps today's fallback rendering, and [P3-T21] records the deviation under Rollout and Follow-up in `spec.md`. The AC7 +logging half is delivered on BOTH surfaces, because it lives in the provider that both surfaces route through. + +**D6 — The AC7 gate is two distinct per-instance structures, not one.** The log gate must never reset, or "once per label +per session" is violated; the suppression signal must reset, or a label that becomes resolvable after a snapshot refresh +stays suppressed forever. [P2-T6] therefore adds two `ConcurrentDictionary` fields with +`StringComparer.OrdinalIgnoreCase`: a reported-labels set that only ever gains entries and gates the `logger.Error` +emission through `TryAdd`, and an absent-labels set that gains an entry when the candidate count is zero and loses it the +moment the same path resolves. A bare `HashSet` is prohibited: ResolveLeafKeyAsync is async and awaits +AcquireSnapshotAsync, so its continuations are not guaranteed to run on one thread. A static set is prohibited by +`spec.md` decision D-B and by .claude/rules/general-unit-test.md, because it is process-wide mutable state shared across +viewers and across test methods in one assembly. Removal on success must be applied in ResolveLeafKeyAsync, not only in the +suffix pass, because the exact-path match at +`UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`:74-77 returns before the suffix pass is reached. + +**D7 — AC6 is additive: the projected score is ADDED alongside the raw score, never substituted for it.** The join key is +built by BreadcrumbRowBuilder.BuildProbabilityIndex at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:210-229, +which assigns through the indexer at :224 rather than calling `Add`, so duplicate keys are tolerated and the last write +wins. A plain substitution would fix the stem-presented case but break the rooted-presented case, which +QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs:118-166 +(Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively) exercises with `fullTarget` = the rooted path and a +score carrying the same rooted path: after a substitution the score key would be the stem while the presented text stayed +rooted, and the percentage would vanish. That test does not assert the percentage, so the regression would ship silently. +[P2-T11] therefore emits both entries and no existing key is ever removed. `BreadcrumbRowBuilder.cs` is NOT modified. + +**D8 — The two #439 Efc router test files require NO HUNK, and that is a re-derived finding rather than an omission.** +`spec.md` lists `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` and +`QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` as MODIFY/retarget. Re-derived in this pass: +every test in both files constructs `new Mock(MockBehavior.Strict)` and supplies the ancestor +chain directly through `ReturnsAsync` (base file lines 30, 125, 261, 311, 385; Activation partial lines 21, 70, 118, 186). +The AC1/AC2 trim lives inside the provider's GetAncestorChainAsync, below that mock boundary, so it cannot reach either +file. What they pin is the router's faithful rendering of whatever chain the provider returns plus the #614 selection +boundaries, both of which this change preserves and both of which are the AC3 invariant the spec carries forward. +Editing them would also be actively harmful: the shared `Chain` helper at base-file lines 434-448 emits a leading +`\Archive` segment that three tests depend on for their own assertions, and +Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection asserts at line 426 that activating segment index 0 yields +`\Archive`, so removing that segment from the fixture would break a #614 boundary test that has nothing to do with this +change while pinning nothing new. [P3-T11] asserts the two files carry no hunk, and [P3-T21] records the disposition. +The retarget that IS load-bearing is the one that drives the real provider — see D9. + +**D9 — The complete retargeting surface is two tests, both re-derived in this pass.** +(1) GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments at +`UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs`:72-91 constructs the real provider and +asserts the full store-rooted chain `\Root`, `\Root\Clients`, `\Root\Clients\Acme` at lines 84-87. It would stay green +untouched only because it constructs the provider WITHOUT a root accessor, which is not how production constructs it; +leaving it that way would pin only the disabled configuration. [P1-T13] rewrites it against a provider configured with a +root accessor, as production configures it, and keeps a companion case for the no-accessor construction. +(2) ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection at +`QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs`:212-243 asserts at lines 219-226 that an EMPTY +archive root strips exactly one leading separator. That is the behaviour AC4 eliminates, so [P1-T14] rewrites the +assertion to the identity projection rather than preserving it. +A sweep of every other test that reads the affected members found no third obligation, and the reason is recorded per +family so a reviewer does not read a non-edit as an oversight: the recents fixtures in +UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs:250-265 and :166-189 and in +UtilitiesCS.Test/OutlookObjects/Folder/FolderRowTests.cs:29-70, :73-101 and :104 all use the RELATIVE recents value +`Recent\One` (and `Recent\Two`) against the root `\\ArchiveRoot`, so the AC5 projection is the identity for them and they +stay green; the two Issue609 suggestion-projection tests at FolderPredictorTests.cs:191-247 already encode exactly the +semantics ToDisplayStem reproduces (in-root full path to stem, relative unchanged, out-of-root full path unchanged, +case-variant to stem) and stay green; the GetOlSubpath assertions at FolderPredictorTests.cs:577-591 and at +ToDoModel.Test, directory Email Utilities, file FolderHandlerTests_Written.cs lines 35-67 both pass a root that is a strict +proper prefix terminated by a separator, which TryMakeArchiveRelative reproduces exactly, and the include-children-false +branch is not converted at all; and every other breadcrumb router test in QuickFiler.Test and UtilitiesCS.Test mocks +IFolderHierarchyProvider, so neither the trim nor the AC7 suppression reaches it (D4, D8). + +**D10 — Persisted-corpus sites and the uncompiled sort utility are deliberately LEFT, per `spec.md` decision D-A sites 5, +6 and 7.** The folder minimal wrapper's relative-path loader at +UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs:56-86 and the folder wrapper source file's loader are unchanged, +because their full-path fallback is depended upon by the persisted relative-path restore branch and by the classifier +corpus, and the wrapper source file is already over the 500-line ceiling. The ToDoModel email-utilities sort file is +unchanged because it is not a Compile item in its project and has no live caller, so converting it would be a no-op unless +the file were first added to the build. Both space-containing paths are named in words only, and this item's footprint +therefore contains no space-containing path. Neither is in the Write Set and neither carries backticks anywhere in this +plan; the absence of backticks on those references is deliberate and must not be "corrected". + +**D11 — File-size budgets, not a blanket ceiling gate.** Two Write Set files are already over the ceiling and one is +exactly at it, so a blanket "at or below 500" assertion would be unsatisfiable on the first and vacuous on the third. +[P3-T10] therefore asserts a per-file budget: +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` at or below 500 (hard, and reachable only because R9's relocation +runs first); +`QuickFiler/Controllers/EfcFormController.cs` at or below 1322, that is baseline 1320 plus at most two lines, because the +single added constructor argument is formatted by CSharpier as one additional line and the collapsed single-line call is +about 126 columns including indent, well past the print width — this file legitimately GROWS and a no-growth assertion on +it would be unsatisfiable; +`UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` at or below 1003, that is no growth, which carries at least eight +lines of slack by derivation: the ProjectSuggestionPath body at lines 848-861 (14 lines) collapses to at most 5, the +include-children branch of GetOlSubpath at lines 955-965 (11 lines) collapses to at most 5, and the two recents projections +add at most 2 lines each; +every other `.cs` file this plan creates or edits at or below 500. + +**D12 — `dotnet-coverage`, not `/EnableCodeCoverage`.** `vstest.console.exe /EnableCodeCoverage` writes a binary `.coverage` +file and the two collectors conflict, so every coverage run in this plan uses +`dotnet-coverage collect --output-format cobertura -- ...`, exactly the form the most recent completed C# bug plan +used. + +**D13 — Comparability, not a repository-wide rate.** The repository-wide Cobertura `line-rate` attribute is not a stable +gate on this harness. The coverage comparison in [P3-T8] is made on four first-party counters produced by ONE pinned +aggregation applied identically to both documents, with `lines-valid` comparability stated as an explicit precondition and +the derived percentages used instead when the denominators differ. The aggregation prints its own success-case output line, +whose exact form is `LINES_COVERED= LINES_VALID= BRANCHES_COVERED= BRANCHES_VALID= PACKAGES_MATCHED=`, so +the values the acceptance conditions read are values the block definitively prints. `PACKAGES_MATCHED` exists so a +package-name mismatch surfaces as a loud zero instead of a silent zero-counter run. + +**D14 — Assembly discovery excludes worktree copies by construction.** The nine first-party test assemblies are named +explicitly on every run command. A path that is never enumerated cannot be loaded, so no worktree under a `.claude` segment +can enter a run. + +--- + +## Write Set and disposition (restated from `spec.md`, lines 652-702) + +| Path | Disposition | +|---|---| +| `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` | CREATE — the shared display projection (AC4, AC5, AC6) | +| `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` | CREATE — the chain trim (AC1, AC2) | +| `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` | MODIFY — 141 lines; trim, optional root accessor, AC2 error, AC7 gate and absence report | +| `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` | MODIFY — 1003 lines, pre-existing over-ceiling; AC4 delegation, AC5 recents, GetOlSubpath true branch | +| `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` | MODIFY — 312 lines; AC4 delegation only (D5) | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | MODIFY — 304 lines; AC6 score projection, AC7 Efc suppression | +| `QuickFiler/Controllers/EfcFormController.cs` | MODIFY — 1320 lines, pre-existing over-ceiling; one argument at line 1053 | +| `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` | CREATE — receives the relocated pipeline helper (R9) | +| `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` | MODIFY — exactly 500 lines; relocation out, then one argument | +| `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` | CREATE | +| `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` | CREATE | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` | CREATE | +| `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` | CREATE | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` | CREATE | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` | MODIFY — 479 lines, budget +21; retarget (D9) | +| `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` | MODIFY — 354 lines; retarget (D9) | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` | NO HUNK — re-derived finding (D8) | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` | NO HUNK — re-derived finding (D8) | +| `UtilitiesCS/UtilitiesCS.csproj` | MODIFY — two new production Compile Include entries | +| `QuickFiler/QuickFiler.csproj` | MODIFY — one new partial Compile Include entry | +| `UtilitiesCS.Test/UtilitiesCS.Test.csproj` | MODIFY — four new test Compile Include entries | +| `QuickFiler.Test/QuickFiler.Test.csproj` | MODIFY — one new test Compile Include entry | + +Out of scope and absent from the Write Set, named in bare prose: anything under the dot-claude, dot-codex or dot-agents +trees; the two published JSON files under the config directory; every GitHub workflow file; the solution file; the +repository-root build property files; the six sibling-owned files named in D1; UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs; +UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs; UtilitiesCS/OutlookObjects/Folder/FolderTreeSnapshotQueries.cs; +UtilitiesCS/OutlookObjects/Folder/IFolderHierarchyProvider.cs; UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs; +the folder wrapper source file; and the ToDoModel email-utilities sort file. + +--- + +## Citation table (re-derived against the current tree in this pass) + +| Repository-relative path | Locator re-derived | +|---|---| +| UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs | 147 lines; three public members at :41, :68, :106; TryMakeArchiveRelative :106-145 with null/whitespace guard :113, trailing-separator trim :118, zero-length root :119-122, equality returning true with an empty stem :124-127, StartsWith prefix test :131, separator-boundary test :137-141, leading-separator trim on the stem :143 | +| `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` | 141 lines; log4net ILog :17-19; `_treeService` :21; single constructor :28-31; GetAncestorChainAsync :34-42; GetImmediateSubfoldersAsync :45-53; ResolveLeafKeyAsync :56-80 with the exact-path early return :74-77; private static ResolveByUniqueSuffix :90-114 with the two-cause emission :108-112; AcquireSnapshotAsync :116-122; MapNodes :124-129; MapNode :131-139 | +| UtilitiesCS/OutlookObjects/Folder/IFolderHierarchyProvider.cs | 65 lines; exactly three members, GetAncestorChainAsync :31-34, GetImmediateSubfoldersAsync :46-49, ResolveLeafKeyAsync :60-63 | +| `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` | 1003 lines; FolderRowArray text-parity XML doc :233-242 and getter :243-... ; AddRecents :788-795 with the unprojected AddRange :793; AddSuggestions :807-811 projecting at :810; AddSuggestionRows :835-846 projecting row text and score at :842-844; ProjectSuggestionPath :848-861 with the unconditional prefix concat :855 and the length guard :858; AddRecentRows :866-882 with the unprojected row construction :879; LoopFolders :903-951 with the ArchiveRootPath fallback :911-914 and the two GetOlSubpath calls :918 and :935; GetOlSubpath :953-971 with the include-children true branch :955-965 | +| UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs | 238 lines; empty-chain single-segment fallback :123-131; presented-text probability lookup :133-135; Classify :152-170; MapSegments 1:1 with no trimming :178-208; BuildProbabilityIndex :210-229 keying on score.FolderPath through the INDEXER at :224, so duplicate keys are tolerated; LeafToken :231-236 | +| UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbSegment.cs | 54 lines; four-argument constructor (key, displayName, folderPath, hasChildren) :29-40; FolderPath :49 | +| UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs | 489 lines, sibling-owned; single-parameter constructor :19-23; SetSuggestionsAsync :29-97 composing the local `built` row list at :42-86 and swapping it under the shared lock at :88-96; SetSuggestionFallbacks :100-119; AddPlainRows :156-168; shared `_sync` :15 and `_suggestionGeneration` :16 | +| UtilitiesCS/Properties/AssemblyInfo.cs | InternalsVisibleTo("UtilitiesCS.Test") :19 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 304 lines; log4net field named `log` :21-23; `_provider` :25; `_boundRoot` :35; five-argument constructor :41-56; public three-argument BindRowsAsync :75-82 forwarding `string.Empty` at :81; internal four-argument BindRowsAsync :92-150 with the bound-root normalization :107-109, the chain loop :110-130, the BuildRows call :132-136 and the AttachSegmentKeys call :137; ToHierarchyPath :152-167 consuming ArchiveStemContract at :157 and :164; AttachSegmentKeys :169-193 indexing presentedRows by row index at :176 | +| QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | 221 lines; FetchChainAsync :50-81 returning null on a null key at :61-64 and on both catch arms at :68-80 | +| `QuickFiler/Controllers/EfcFormController.cs` | 1320 lines; ConfigureBreadcrumbControl :1047-1067 with the provider construction :1053-1055; BindBreadcrumbRowsAsync :1111-1128 with the raw score read :1115-1117 and the four-argument router call :1118 | +| `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` | EXACTLY 500 lines; usings :1-22; `internal partial class QfcItemController` :26; `_breadcrumbViewer` field :28; EnsureBreadcrumbPipeline comment :132-136, ExcludeFromCodeCoverage attribute :137, member :138-163, provider construction :147-149, arrow-event rewiring :153-162 | +| `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` | 312 lines; AssignFolderComboBox :191-250 with EnsureBreadcrumbPipeline :206, AddFolderItems :212, SetFolderSuggestions :221 and the projection call :231-234; the duplication rationale comment :223-230; ProjectPredeterminedFolder XML doc :252-271 and body :272-285 | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` | 479 lines, 21 lines of headroom; namespace UtilitiesCS.Test.OutlookObjects.Folder :11; Archive fixture keys :36-70; GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments :72-91 with the root-to-leaf assertion :84-87; 13 provider constructions at :76, 98, 118, 139, 157, 175, 193, 218, 240, 269, 299, 316, 338; single-null-argument construction :316; suffix-resolution tests :236-251 and :295-310 | +| UtilitiesCS.Test/OutlookObjects/Folder/FolderHierarchyProviderAdapterTests.cs | 258 lines; 6 provider constructions at :98, 122, 142, 170, 185, 202 | +| UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs | 1066 lines, pre-existing over-ceiling and NOT touched; FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents :165-189 with the relative recent `Recent\One` :175; Issue609 projection tests :191-247; AddRecents_WhenRecentsExist_AppendsHeaderAndEntries :249-265 with relative recents :255; GetOlSubpath assertions :577-591 | +| UtilitiesCS.Test/OutlookObjects/Folder/FolderRowTests.cs | text-parity test :29-70 with the relative recent :39; FolderRowArray_DoesNotAlterFolderArrayOutput :73-101; FindFolderRows parity :104; recents mock helper :248-255 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` | 455 lines; namespace QuickFiler.Test.Controllers :12; strict provider mocks :30, 125, 261, 311, 385; lineage test :20-116 asserting the archive-root index :109 and :113 and the 73 percent cell :114; rooted-target test :118-166 binding a rooted score at :149; boundary test :302-377 building an inline Archive segment :327; slash-only-root test :379-427 asserting segment-0 activation yields `\Archive` :426; shared Chain helper :434-448 emitting the leading Archive segment :444; Segment helper :450-453 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` | 253 lines; strict provider mocks :21, 70, 118, 186; four uses of the shared Chain helper :33, 82, 130, 196 | +| `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` | 354 lines; namespace QuickFiler.Controllers.Tests :10; `public partial class QfcItemController_FolderHandlingTests` :21; ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection :212-243 with the empty-root one-separator assertion :219-226 and the case-insensitive assertion :239-242; the FolderContains-boundary empty-root test doc :245-258 | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs | 358 lines; namespace QuickFiler.Test.Controllers :16; rooted/relative pair bind :254-259 | +| QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs | BindBreadcrumbRowsAsync_WhenArchiveRootThrows_ReportsOnceAndDoesNotThrow :242 | +| `UtilitiesCS/UtilitiesCS.csproj` | ArchiveStemContract.cs Compile Include :623; BreadcrumbRowBuilder.cs :625; OutlookFolderHierarchyProvider.cs :640; FolderPredictor.cs :808; the EnsureNuGetPackageBuildImports Error target :1293; Analyzer Include block :1301-1310 | +| `UtilitiesCS.Test/UtilitiesCS.Test.csproj` | folder-test Compile Include block :276-307, with ArchiveStemContractTests.cs :281, FolderHierarchyProviderAdapterTests.cs :302, FolderTreeSnapshotQueriesAncestorChainTests.cs :303 and OutlookFolderHierarchyProviderTests.cs :304; EnsureNuGetPackageBuildImports Error :946; Analyzer Include block :934-966 | +| `QuickFiler/QuickFiler.csproj` | BreadcrumbBridgeRouter.cs Compile Include :291; QfcItemController.ViewerSetup.cs :335; QfcItemController.FolderHandling.cs :338; EnsureNuGetPackageBuildImports Error :586; Analyzer Include block :592-601 | +| `QuickFiler.Test/QuickFiler.Test.csproj` | BreadcrumbBridgeRouterIssue439Tests.cs Compile Include :64; QfcItemController.FolderHandlingTests.Part2.cs :182; EnsureNuGetPackageBuildImports Error :501; Analyzer Include block :489-521 | +| `global.json` | SDK 8.0.205 pinned :3 with paths ".dotnet-sdk" and "$host$" :6-9 and the error message naming the repo-local install script :10 | +| `.csharpierignore` | evidence exclusion :4; cobertura :5; coverage :6; trx :8; project-file exclusion rationale :9-14 with `*.csproj` :12 | +| `.gitignore` | test-results bracket class `[Tt]est[Rr]esult*/` :39; `artifacts/` :57; `coverage/*` :144 | +| `coverage.config` | ModulePaths Exclude block :12-22 carrying no Test.dll entry, so the derived config appends one | +| .claude/hooks/enforce-evidence-locations.ps1 | artifacts/csharp/ named as permitted :26; forbidden prefixes :64-77 | +| `spec.md` | Acceptance Criteria AC1 through AC8 at lines 834-841; Write Set at lines 645-725; decision D-A at lines 283-338; decision D-B at lines 340-383; decision D-C at lines 385-407; decision D-D at lines 409-446; Test Strategy at lines 759-830 | + +--- + +### Phase 0 — Baseline capture and toolchain bootstrap + +This worktree is NOT bootstrapped. Neither the repository-local SDK tree nor the NuGet packages tree is present, so every +`dotnet` command and every `msbuild` invocation fails until [P0-T3] and [P0-T4] have run. A missing-packages MSBuild failure +must never be recorded as "the analyzer gate is already red at the merge base": each project declares an +EnsureNuGetPackageBuildImports target whose Error fires at BeforeTargets PrepareForBuild +(`UtilitiesCS/UtilitiesCS.csproj`:1293, `QuickFiler/QuickFiler.csproj`:586, `UtilitiesCS.Test/UtilitiesCS.Test.csproj`:946, +`QuickFiler.Test/QuickFiler.Test.csproj`:501), which is a bootstrap failure and nothing else. + +- [ ] [P0-T1] Read, in the `policy-compliance-order` sequence, `CLAUDE.md`, .claude/rules/general-code-change.md, .claude/rules/general-unit-test.md, .claude/rules/csharp.md, and .claude/rules/tonality.md, then write `/evidence/baseline/phase0-instructions-read.md` containing the literal field lines `Timestamp:`, `Policy Order:`, and an explicit list of the five files read with their line counts. Acceptance: the artifact exists and contains all five paths and the three field lines. + +- [ ] [P0-T2] Record the branch and base commit into `/evidence/baseline/p0-t2-base.md`, including the literal field lines `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`, and the two derived lines `BASE-BRANCH: ` and `BASE-SHA: <40-hex>`. Acceptance: both derived lines are present and `BASE-SHA` is a 40-character lowercase hexadecimal value. + +```powershell +git rev-parse --abbrev-ref HEAD +git rev-parse HEAD +git status --porcelain --untracked-files=all +``` + +- [ ] [P0-T3] Install the repository-local .NET SDK with scripts\vscode\Install-RepoDotNetSdk.ps1 and record `/evidence/baseline/p0-t3-sdk.md`. `global.json` pins SDK 8.0.205 with the search paths ".dotnet-sdk" then "$host$", and no host SDK on this machine satisfies that pin, so every `dotnet` command fails until this task completes. Record the existence of the .dotnet-sdk directory BEFORE and AFTER the command, so the artifact is truthful whether the tree was absent or already present. Acceptance: after the command, the directory .dotnet-sdk\sdk\8.0.205 exists and `dotnet --version` prints a version beginning `8.0.`; the artifact records both the before/after existence booleans and the printed version. + +```powershell +pwsh -NoProfile -File scripts\vscode\Install-RepoDotNetSdk.ps1 +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +dotnet --version +Test-Path '.dotnet-sdk\sdk\8.0.205' +``` + +- [ ] [P0-T4] Restore NuGet packages for the solution and record `/evidence/baseline/p0-t4-restore.md`. The packages tree is not present in this worktree, so this is a bootstrap step and not a repair. Record the count of packages subdirectories before and after the command, and the resolution status of every Analyzer Include HintPath declared by the four Write Set project files, because an unresolved analyzer path is CS0006, an error, and would fail [P0-T9] and [P0-T10] for a reason unrelated to this change. Analyzer version parity between the project files and packages.config was measured as clean before planning (Meziantou.Analyzer 3.0.203 and Roslynator.Analyzers 5.0.0 on both sides), so this probe is a verification step and no back-fill is planned. Acceptance: the artifact records the restore `EXIT_CODE:`, the before and after subdirectory counts, and one `RESOLVED:` or `UNRESOLVED:` line per analyzer path with zero `UNRESOLVED:` lines. + +```powershell +$before = if (Test-Path 'packages') { (Get-ChildItem -Path 'packages' -Directory).Count } else { 0 } +msbuild TaskMaster.sln /t:Restore /m /p:RestorePackagesConfig=true /p:Configuration=Debug "/p:Platform=Any CPU" +$after = (Get-ChildItem -Path 'packages' -Directory).Count +"packages-subdirs before=$before after=$after" +foreach ($proj in @('UtilitiesCS\UtilitiesCS.csproj', 'QuickFiler\QuickFiler.csproj', 'UtilitiesCS.Test\UtilitiesCS.Test.csproj', 'QuickFiler.Test\QuickFiler.Test.csproj')) { + [xml]$p = Get-Content -LiteralPath $proj + foreach ($a in $p.SelectNodes('//*[local-name()="Analyzer"]')) { + $hint = $a.GetAttribute('Include') + $full = Join-Path (Split-Path -Parent $proj) $hint + if (Test-Path -LiteralPath $full) { "RESOLVED: $hint" } else { "UNRESOLVED: $hint" } + } +} +``` + +- [ ] [P0-T5] Restore the manifest-pinned dotnet tools and record `/evidence/baseline/p0-t5-tools.md`. Acceptance: the artifact records `EXIT_CODE: 0` and `dotnet tool run csharpier --version` prints `1.2.6`. + +```powershell +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +dotnet tool restore +dotnet tool run csharpier --version +``` + +- [ ] [P0-T6] Resolve `dotnet-coverage` and record `/evidence/baseline/p0-t6-dotnet-coverage.md`. Run `dotnet-coverage --version` first; only if that probe exits non-zero, run `dotnet tool install --global dotnet-coverage` and re-probe. Acceptance: the artifact records a final `dotnet-coverage --version` invocation with `EXIT_CODE: 0` and the printed version string, and states which of the two branches was taken. + +```powershell +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +dotnet-coverage --version +``` + +- [ ] [P0-T7] Resolve vstest.console.exe through vswhere and record the full resolved path into `/evidence/baseline/p0-t7-vstest.md` as the derived line `VSTEST-PATH: `. This is the one artifact exempted from R3's path reduction, because pinning the resolved path is the task's purpose. Acceptance: `VSTEST-PATH` names an existing file and the artifact records `EXIT_CODE: 0`. + +```powershell +$vswhere = "${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 +$vstest +Test-Path $vstest +``` + +- [ ] [P0-T8] Capture the CSharpier baseline into `/evidence/baseline/p0-t8-csharpier.md`, recording the verbatim printed line and the derived line `BASELINE-CSHARPIER-CHECKED-FILES: `. The success-case output of this command on a clean tree is the single line of the form `Checked files in ms.` with exit 0; `check` is read-only and returns non-zero on drift, so the exit code is the gate here. If the check reports drift, the artifact must list every drifting path as a disclosed pre-existing set. Acceptance: the artifact records `EXIT_CODE:`, the printed line, and the `BASELINE-CSHARPIER-CHECKED-FILES` numeral. + +```powershell +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +dotnet tool run csharpier check . +``` + +- [ ] [P0-T9] Capture the analyzer-build baseline into `/evidence/baseline/p0-t9-analyzers.md` using exactly the CLAUDE.md analyzer command. Acceptance: the artifact records `EXIT_CODE:` and an `Output Summary:` giving the warning and error counts read from the MSBuild summary, and states explicitly that [P0-T4] completed first so a bootstrap failure cannot be misread as an analyzer failure. + +```powershell +msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +``` + +- [ ] [P0-T10] Capture the nullable-build baseline into `/evidence/baseline/p0-t10-nullable.md` using exactly the CLAUDE.md nullable command. `/p:Nullable=enable` must not be added and `/t:Build` must not be substituted. Acceptance: the artifact records `EXIT_CODE:` and the warning and error counts. + +```powershell +msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +``` + +- [ ] [P0-T11] Run the UtilitiesCS.Test and QuickFiler.Test assemblies and record their pass/fail counts into `/evidence/baseline/p0-t11-suites.md` as the derived lines `BASELINE-UT-TOTAL:`, `BASELINE-UT-PASSED:`, `BASELINE-UT-FAILED:`, `BASELINE-QFT-TOTAL:`, `BASELINE-QFT-PASSED:`, `BASELINE-QFT-FAILED:`, read from the TRX `ResultSummary/Counters` element of each run. Do not paste TRX content (R3). Acceptance: all six derived lines are present and numeric, each `BASELINE-*-FAILED` recorded whatever its value, and the artifact names the four excluded shell-icon classes (R13). + +```powershell +$vswhere = "${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 +& $vstest UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p0-t11-ut' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests' +& $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p0-t11-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook' +``` + +- [ ] [P0-T12] Run the full nine-assembly suite under `dotnet-coverage` and record `/evidence/baseline/p0-t12-coverage.md` with the derived lines `BASELINE-LINES-COVERED:`, `BASELINE-LINES-VALID:`, `BASELINE-BRANCHES-COVERED:`, `BASELINE-BRANCHES-VALID:`, `BASELINE-PACKAGES-MATCHED:`, the two derived percentages, and `BASELINE-TOTAL-TESTS:` / `BASELINE-FAILED-TESTS:`. The four counters come from the pinned aggregation block below, whose success-case output is the single line it prints itself, of the form `LINES_COVERED= LINES_VALID= BRANCHES_COVERED= BRANCHES_VALID= PACKAGES_MATCHED=` (D13). Record `BASELINE_FLOOR: MET` or `BASELINE_FLOOR: NOT MET` against the 80 percent line floor and continue either way; a pre-existing repository floor never halts this plan. Acceptance: the five `BASELINE-` counter lines are present and numeric and `BASELINE-PACKAGES-MATCHED` is greater than zero. + +```powershell +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +$vswhere = "${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 +New-Item -ItemType Directory -Force -Path 'coverage' | Out-Null +$derived = 'coverage\799-effective-coverage.config' +[xml]$cfg = Get-Content -LiteralPath 'coverage.config' +$excl = $cfg.Configuration.CodeCoverage.ModulePaths.Exclude +$node = $cfg.CreateElement('ModulePath'); $node.InnerText = '.*\.Test\.dll$' +$null = $excl.AppendChild($node); $cfg.Save((Join-Path (Get-Location) $derived)) +dotnet-coverage collect --output coverage\799-baseline.cobertura.xml --output-format cobertura --settings coverage\799-effective-coverage.config -- $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll SVGControl.Test\bin\Debug\SVGControl.Test.dll Tags.Test\bin\Debug\Tags.Test.dll TaskMaster.Test\bin\Debug\TaskMaster.Test.dll TaskTree.Test\bin\Debug\TaskTree.Test.dll TaskVisualization.Test\bin\Debug\TaskVisualization.Test.dll ToDoModel.Test\bin\Debug\ToDoModel.Test.dll UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll VBFunctions.Test\bin\Debug\VBFunctions.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p0-t12' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests' +$doc = New-Object System.Xml.XmlDocument +$doc.Load((Resolve-Path -LiteralPath 'coverage\799-baseline.cobertura.xml').Path) +$first = @('QuickFiler','SVGControl','Tags','TaskMaster','TaskTree','TaskVisualization','ToDoModel','UtilitiesCS','VBFunctions') +$lc=0;$lv=0;$bc=0;$bv=0;$pm=0 +foreach ($pkg in $doc.SelectNodes('//package')) { + $name = $pkg.GetAttribute('name') + "PACKAGE: $name" + if ($first -notcontains $name) { continue } + $pm++ + foreach ($ln in $pkg.SelectNodes('.//line')) { + $lv++ + if ([int]$ln.GetAttribute('hits') -gt 0) { $lc++ } + $cc = $ln.GetAttribute('condition-coverage') + if ($cc -match '\((\d+)/(\d+)\)') { $bc += [int]$Matches[1]; $bv += [int]$Matches[2] } + } +} +"LINES_COVERED=$lc LINES_VALID=$lv BRANCHES_COVERED=$bc BRANCHES_VALID=$bv PACKAGES_MATCHED=$pm" +``` + +- [ ] [P0-T13] Determine, from coverage\799-baseline.cobertura.xml, which Write Set production files are measurable, and write `/evidence/baseline/p0-t13-measurability.md`. For each of the seven EXISTING Write Set production paths, query for a `class` element whose `filename` attribute ends with a directory separator followed by that file's name, and record one line per file of the form `MEASURABLE: ` or `UNMEASURABLE: `. The trailing-name match must be separator-anchored, because an unanchored suffix over-selects a sibling whose name merely ends with the same characters. The two files this plan CREATES are recorded separately as `NEW: ` and are measured for the first time by [P3-T9]. Acceptance: exactly seven `MEASURABLE:`/`UNMEASURABLE:` lines are present, one per existing Write Set production path, and the artifact records the class-element counts the determination was made from. + +```powershell +$doc = New-Object System.Xml.XmlDocument +$doc.Load((Resolve-Path -LiteralPath 'coverage\799-baseline.cobertura.xml').Path) +$names = @('OutlookFolderHierarchyProvider.cs','FolderPredictor.cs','QfcItemController.FolderHandling.cs','BreadcrumbBridgeRouter.cs','EfcFormController.cs','QfcItemController.ViewerSetup.cs','ArchiveStemContract.cs') +foreach ($n in $names) { + $hit = 0 + foreach ($c in $doc.SelectNodes('//class')) { + $f = $c.GetAttribute('filename') + if ($f.EndsWith('\' + $n) -or $f.EndsWith('/' + $n)) { $hit++ } + } + "$n classElements=$hit" +} +``` + +- [ ] [P0-T14] Record the baseline line count of every file this plan edits or creates into `/evidence/baseline/p0-t14-sizes.md`, one ` = ` line per file, covering the seven existing Write Set production paths, the four existing Write Set test paths, and the two #439 test paths D8 marks NO HUNK, plus a `CEILING: 500 (applies to *.cs only)` line. Record the four project files separately under a `PROJECT-FILE (exempt): = ` heading with the R8 reason. Record the three pre-existing over-ceiling files under a `PRE-EXISTING OVER CEILING:` heading with the D11 budgets. Acceptance: every `.cs` path has a numeric count; the four project-file counts are recorded under the exempt heading with their reason; the artifact states that `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is exactly 500 and that `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` has 21 lines of headroom. + +- [ ] [P0-T15] Record the pre-change status of the tests this plan retargets or must keep green into `/evidence/baseline/p0-t15-tests.md`, one line per test of the form `BASELINE-PASS: ` or `BASELINE-FAIL: `, derived from the two TRX documents [P0-T11] wrote. The set is: the two retarget targets named in D9 (GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments and ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection); the recents and projection tests named in D9 that must stay green (FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents, AddRecents_WhenRecentsExist_AppendsHeaderAndEntries, Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths, Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath, FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds, GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment); and the five #439 router tests D8 marks NO HUNK (Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability, Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively, Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows, Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic, Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection). This is the set that makes the Phase 2 no-newly-failing comparison meaningful. Acceptance: thirteen `BASELINE-PASS:` or `BASELINE-FAIL:` lines are present, one per named test, each derived from a TRX this plan wrote. + +--- + +### Phase 1 — Declaration seams and failing regression tests + +Phase 1 is test-first. Tasks [P1-T1] through [P1-T5] add only type-level declarations and their Compile entries, because the +new tests name types and members that do not exist yet and a missing declaration reddens the whole test assembly at compile +time rather than producing a targeted failure. No production behaviour changes in Phase 1: every seam body either throws +`NotImplementedException` or stores a value nothing reads yet. + +- [ ] [P1-T1] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveStemProjection` and the single member `public static string ToDisplayStem(string folderPath, string archiveRoot)` whose body is `throw new NotImplementedException("Issue #799: the display projection body is supplied by [P2-T1].");`. The XML doc must state that this is a LENIENT DISPLAY projection returning the archive-relative stem when the path is strictly under the root and the input unchanged in every other case including an empty or whitespace root, and must state why it is a separate type rather than a member on the strict contract: that contract is a hard boundary that yields an empty string on failure and never passes its input through, and every display site needs the opposite fallback, so a lenient overload there would blur the invariant #614 created it for. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `ToDisplayStem`. + +- [ ] [P1-T2] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveChainProjection` and the single member `public static bool TryTrimBelowArchiveRoot(IReadOnlyList chain, string archiveRoot, out IReadOnlyList trimmed)` whose body assigns an empty array to `trimmed` and then is `throw new NotImplementedException("Issue #799: the chain trim body is supplied by [P2-T2].");`. The XML doc must state that the archive-root node is the first chain index whose segment FolderPath is the root itself, detected as TryMakeArchiveRelative returning true with an empty stem, that the method returns the remainder of the chain after that index, and that it returns false when no such index exists and also when that index is the last one, because the leaf is then the root and there is nothing to render below it. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `TryTrimBelowArchiveRoot`. + +- [ ] [P1-T3] Add two one-line self-closing Compile Include entries to `UtilitiesCS/UtilitiesCS.csproj` for the two new production helpers, adjacent to the existing ArchiveStemContract.cs entry at line 623. The project is legacy non-SDK with an insertion-ordered item list and no globbing, so the entries are mandatory and their placement follows the neighbouring folder entries. Acceptance: the project file contains exactly two new Compile Include lines and the two named files compile. + +- [ ] [P1-T4] In `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`, add the declaration-only seams that Phase 1 tests bind against, changing no existing behaviour. Add `public interface IFolderLabelAbsenceReport` with the single member `bool IsAbsentLabel(string folderPath);` and an XML doc recording D4's reason for a separate interface rather than a fourth member on IFolderHierarchyProvider. Declare that the provider implements it, with `public bool IsAbsentLabel(string folderPath) => throw new NotImplementedException("Issue #799: the absence report body is supplied by [P2-T6].");`. Add the optional second constructor parameter `System.Func archiveRootAccessor = null`, stored in the internal get-only auto-property `ArchiveRootAccessor` (D3), leaving the existing null check on the first parameter exactly as it is. Add `internal System.Action ErrorSink { get; set; }` with an XML doc naming it the injected diagnostic sink that tests observe instead of attaching a log4net appender, so no test mutates the process-global logger repository. Do not change GetAncestorChainAsync, GetImmediateSubfoldersAsync, ResolveLeafKeyAsync or ResolveByUniqueSuffix in this task. Acceptance: the solution compiles; the constructor declares exactly two parameters, the second optional with a null default; and a case-sensitive search of the file finds the single-line tokens `IFolderLabelAbsenceReport`, `ArchiveRootAccessor` and `ErrorSink`. + +- [ ] [P1-T5] Build the solution so the seam declarations are available to the test projects, and record `/evidence/regression-testing/p1-t5-seam-build.md`. Acceptance: `EXIT_CODE: 0`, which also proves D2's claim that the optional parameter leaves all 19 existing provider constructions compiling unchanged, including the single-null-argument construction at UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs:316. + +```powershell +msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" +``` + +- [ ] [P1-T6] Create `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` with `[TestClass] public sealed class ArchiveStemProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, using MSTest and FluentAssertions and no mocks, covering the #614 boundary cases the spec enumerates: a path strictly under the root; a path EQUAL to the root, which must return the input unchanged rather than an empty string; the Archive2 false-prefix case, where a path under a sibling folder named Archive2 tested against a root ending in Archive yields the character 2 at the root's length and is therefore NOT projected; a root supplied with one and with two trailing separators; an empty root and a whitespace-only root, both of which must yield the input unchanged, which is the AC4 one-separator strip elimination; a null path and an empty path; forward-slash separators on both parameters; and a mixed-case root. Acceptance: the file compiles once [P1-T10] wires it, contains at least nine `[TestMethod]` attributes, and is at or below 500 lines. + +- [ ] [P1-T7] Create `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` with `[TestClass] public sealed class ArchiveChainProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, building every chain from `FolderBreadcrumbSegment` literals through the four-argument constructor at UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbSegment.cs:29-40, with no snapshot, no provider and no COM. It covers: a chain that passes through the root, asserting the returned segments are exactly the ones after the root node and that segment identity is preserved by reference; a chain that does not pass through the root, asserting false and an empty output; a chain whose LEAF is the root, asserting false; an empty chain; a single-element chain that is the root; a root supplied with a trailing separator; and the Archive2 false-prefix case at chain level. Acceptance: the file compiles once [P1-T10] wires it, contains at least seven `[TestMethod]` attributes, and is at or below 500 lines. + +- [ ] [P1-T8] Create `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` with `[TestClass] public sealed class OutlookFolderHierarchyProviderTrimTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, using a `Mock` returning a hand-built `FolderTreeSnapshot` in the pattern already used throughout UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs, plus a `Func` root accessor. It contains: `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` (a store-rooted three-level Archive chain; assert the returned segments begin at the first node BELOW the archive root and that neither the store node nor the archive-root node appears); `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain` (the no-accessor construction is the effective off switch); `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty` (asserting the AC2 error text is delivered exactly once through the injected `ErrorSink` and that the result is an empty segment list, which routes each surface into its existing fallback); `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty`; `GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain` (the accessor is lazy precisely because the archive-root property throws, per D2); `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence` (resolve an unresolvable label twice against the same provider instance, assert exactly one emission through `ErrorSink`, and assert `IsAbsentLabel` returns true for that path); `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` (a decoy snapshot in which two nodes share the suffix; assert one emission and that `IsAbsentLabel` returns FALSE, which is decision D-B's zero-candidate restriction); and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` (D6's reset). Acceptance: the file compiles once [P1-T10] wires it, contains exactly eight `[TestMethod]` attributes, and is at or below 500 lines. + +- [ ] [P1-T9] Create `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` with `[TestClass] public sealed class FolderPredictorRecentsProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, following the globals/recents mock construction already used at UtilitiesCS.Test/OutlookObjects/Folder/FolderRowTests.cs:246-258 and UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs:994. It contains `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`, `FolderRowArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`, `FolderRowArray_AndFolderArray_AgreeOnRecentTextAfterProjection` (the text-parity contract documented at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`:233-242 and currently unasserted), and `FolderArray_OutOfRootRecentEntry_IsLeftUnchanged`. Each test seeds the recents list with one archive-rooted entry and one already-relative entry, so the projection is observable and the identity case is pinned in the same fixture. Acceptance: the file compiles once [P1-T10] wires it, contains exactly four `[TestMethod]` attributes, and is at or below 500 lines. + +- [ ] [P1-T10] Add four one-line self-closing Compile Include entries to `UtilitiesCS.Test/UtilitiesCS.Test.csproj` for the four new test files, appended adjacent to the existing folder-test entries at lines 302-304. The project is legacy non-SDK with an insertion-ordered item list and no globbing. Acceptance: the project file contains exactly four new Compile Include lines and the four named files compile. + +- [ ] [P1-T11] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` with `[TestClass] public sealed class BreadcrumbBridgeRouterScoreJoinTests` in namespace `QuickFiler.Test.Controllers`, modelled on the strict-mock construction already used at QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs:254-259 (a `Mock`, a `Mock` capturing NavigateToString output, a real `BreadcrumbMessageCodec`, a real `BreadcrumbHtmlRenderer` and a real `BreadcrumbOutboundQueue`). It contains: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` (the AC6 pin: presented row is the archive-relative stem, the score carries the raw rooted path, the bound root is non-empty; assert the rendered document contains the percentage cell for that row); `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage` (D7's additive requirement: the rooted-presented case must not regress); `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` (the public three-argument overload forwards an empty root, so the projection is the identity and no existing caller changes behaviour); `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` (the AC3 pin, asserted as its own test and not as an incidental consequence: with an ancestor chain that begins below the archive root, the bound row's filing target and the joined score key are both still the archive-relative stem); and `BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly` (the spec's integration scenario, driven entirely through the router with no WebView2 and no Outlook: a banner row, a suggestion row, a search-result row, the trash pseudo-row and one stale label; assert lineage on both folder row kinds, the existing fallback on the stale label, and no lineage on the banner or trash rows). Every provider interaction is Moq-supplied, so the AC7 suppression path is inert in this file by construction (D4). Acceptance: the file compiles once [P1-T12] wires it, contains exactly five `[TestMethod]` attributes, and is at or below 500 lines. + +- [ ] [P1-T12] Add one one-line self-closing Compile Include entry to `QuickFiler.Test/QuickFiler.Test.csproj` for the new test file, appended adjacent to the existing BreadcrumbBridgeRouterIssue439Tests.cs entry at line 64. Acceptance: the project file contains exactly one new Compile Include line and the named file compiles. + +- [ ] [P1-T13] Retarget GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments in `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` lines 72-91 so the pinned configuration is the one production uses (D9): rename it to `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`, construct the provider with a root accessor returning the fixture's archive-root path, and replace the root-to-leaf assertion at lines 84-87 with the trimmed expectation. Immediately after it, add the companion case `GetAncestorChainAsync_WithoutRootAccessor_ReturnsRootToLeafSegments` retaining the original untrimmed expectation for the no-accessor construction, so the off switch stays pinned. Retargeting rather than deleting is required because the deleted behaviour is a specification change and a deleted test pins nothing. The file is 479 lines and the ceiling is 500, so the net addition must not exceed 21 lines; the original body is reused for the companion case rather than duplicated wholesale. Acceptance: a case-sensitive search of the file finds zero matches for the single-line token `GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments` and one match each for `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath` and `GetAncestorChainAsync_WithoutRootAccessor_ReturnsRootToLeafSegments`; the twelve other provider constructions in the file are unchanged; and the file is at or below 500 lines. + +- [ ] [P1-T14] Retarget the empty-root assertion in `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` lines 219-226, inside ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection, so it asserts the identity projection rather than the one-separator strip AC4 eliminates: an empty archive root must now leave the input unchanged, and the assertion's `because` text must state that AC4 of issue #799 removed the empty-root strip. Update the surrounding XML doc at lines 206-211 so it no longer describes the removed behaviour, and update the XML doc at lines 245-258 of the following test, whose prose asserts that an empty archive root causes FolderArray entries to be stripped; that following test's own assertions are re-derived against the new behaviour and updated only where they encode the removed strip. The five other boundary assertions in the retargeted test (null root, null path, out-of-root path, empty-remainder guard, case-insensitive prefix) are unchanged, because ToDisplayStem reproduces each of them. Acceptance: a case-sensitive search of the file finds zero matches for the single-line literal `archivePrefix of one separator, which it strips`; the retargeted test still contains exactly six assertions on ProjectPredeterminedFolder; and the file is at or below 500 lines. + +- [ ] [P1-T15] Build the solution with the new and retargeted tests in place and record `/evidence/regression-testing/p1-t15-test-build.md`. Acceptance: `EXIT_CODE: 0`, proving every new test compiles against the Phase 1 seams. + +```powershell +msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" +``` + +- [ ] [P1-T16] [expect-fail] Run the four new and one retargeted UtilitiesCS.Test classes and record `/evidence/regression-testing/p1-t16-ut-fail.md` with `ExpectedExitCode: 1`. The artifact must enumerate, by fully qualified name, every failing test and state for each whether it fails because a Phase 1 seam throws `NotImplementedException` (tag `SEAM-BLOCKED`) or because the production behaviour is not yet changed (tag `NEW` or `RETARGETED`). Acceptance: `EXIT_CODE: 1`, and the recorded failure set includes `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` and `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`. + +```powershell +$vswhere = "${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 +& $vstest UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p1-t16' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~ArchiveStemProjectionTests|FullyQualifiedName~ArchiveChainProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTrimTests|FullyQualifiedName~FolderPredictorRecentsProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTests' +``` + +- [ ] [P1-T17] [expect-fail] Run the new QuickFiler.Test score-join class and the retargeted folder-handling class and record `/evidence/regression-testing/p1-t17-qft-fail.md` with `ExpectedExitCode: 1`, enumerating each failing test by fully qualified name with its failure message reduced per R3. Acceptance: `EXIT_CODE: 1`, and both `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` and `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` appear in the failure set. + +```powershell +$vswhere = "${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 +& $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p1-t17' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterScoreJoinTests|FullyQualifiedName~QfcItemController_FolderHandlingTests' +``` + +- [ ] [P1-T18] Write `/evidence/regression-testing/p1-t18-red-inventory.md` consolidating the two fail-before artifacts into one list of every test that is red at the end of Phase 1, each tagged `NEW`, `RETARGETED` or `SEAM-BLOCKED`. This is the set Phase 2 must turn green and nothing else. Acceptance: the inventory's entry count equals the sum of the failure counts recorded by [P1-T16] and [P1-T17], and every entry carries exactly one of the three tags. + +--- + +### Phase 2 — Production implementation + +- [ ] [P2-T1] Replace the [P1-T1] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` with the real implementation: return the stem when `ArchiveStemContract.TryMakeArchiveRelative(folderPath, archiveRoot, out var stem)` returns true AND `stem.Length > 0`, and return `folderPath` unchanged in every other case. The length condition is what makes "path equals root" non-projectable, because the contract returns true with an empty stem on exact equality at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:124-127 and an empty display row is worse than the full path; it reproduces the existing guard at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`:858 exactly. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the display projection body is supplied by`. + +- [ ] [P2-T2] Replace the [P1-T2] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` with the real implementation: scan the chain for the first index whose segment FolderPath satisfies `ArchiveStemContract.TryMakeArchiveRelative(segment.FolderPath, archiveRoot, out var stem)` returning true with `stem.Length == 0`, which is exactly the equality case; assign the remainder after that index to `trimmed` and return true; return false with an empty `trimmed` when no such index exists, when the chain is null or empty, and when that index is the last element. A null or whitespace `archiveRoot` returns false, which follows from the contract's own guard at ArchiveStemContract.cs:113 and needs no separate branch. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the chain trim body is supplied by`. + +- [ ] [P2-T3] Relocate the breadcrumb pipeline helper OUT of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` and INTO the new file `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task runs BEFORE [P2-T14] adds the constructor argument; the reverse order puts the viewer-setup file at 501 lines in an intermediate state (R9). Move lines 132-163 verbatim — the two-part comment at 132-136, the `[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]` attribute at 137, and the member body at 138-163 — together with the blank line that separated it from its neighbour. The new file declares `internal partial class QfcItemController` in namespace `QuickFiler.Controllers` and carries only the using directives the moved code needs, which are the ones for the concrete viewer type and for the arrow-event handler; the provider is referenced by its fully qualified name in the moved code and needs no using. This is a pure relocation: no statement is added, removed or reordered. Acceptance: a case-sensitive search of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` returns zero matches for the single-line token `EnsureBreadcrumbPipeline` and that file's line count is at least 30 lines lower than the [P0-T14] baseline of 500; a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` returns one match for that token and one for the single-line token `ExcludeFromCodeCoverage`. + +- [ ] [P2-T4] Add one one-line self-closing Compile Include entry to `QuickFiler/QuickFiler.csproj` for the new item-controller partial, adjacent to the existing QfcItemController.ViewerSetup.cs entry at line 335. Acceptance: the project file contains exactly one new Compile Include line and the solution compiles, which is the only proof that the relocated member is still in the build. + +- [ ] [P2-T5] Apply the AC1 and AC2 trim inside GetAncestorChainAsync at `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` lines 34-42, after the snapshot walk and before segment mapping, so row order, banner placement and the trash pseudo-row are untouched. Read the root lazily through the [P1-T4] `ArchiveRootAccessor` property inside a try that treats any exception from the accessor as "no trim configured", which is what keeps the existing archive-root-throws behaviour intact (D2). When the accessor is null or yields a null, empty or whitespace root, return the mapped chain unchanged, which is the effective off switch. Otherwise call `ArchiveChainProjection.TryTrimBelowArchiveRoot`; on true, map and return the trimmed segments; on false, emit one error through the log4net ILog already declared at lines 17-19 AND through the [P1-T4] `ErrorSink`, and return `Array.Empty()`, which routes the Efc surface into the empty-chain single-segment fallback at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:123-131 and the QuickFiler surface into its existing scored fallback. Do not modify GetImmediateSubfoldersAsync: subfolders are below the leaf and therefore below the archive root by construction. Acceptance: `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot`, `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain`, `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty`, `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty` and `GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain` all pass, and the two retargeted provider tests from [P1-T13] both pass. + +- [ ] [P2-T6] Implement the AC7 log gate and absence classification in `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`. Change ResolveByUniqueSuffix at lines 90-114 from `private static` to a private instance member so it can reach per-instance state, and add the two `ConcurrentDictionary` fields D6 requires, both with `StringComparer.OrdinalIgnoreCase`: a reported-labels set that gates the existing `logger.Error` emission at lines 108-112 through `TryAdd`, so a label already reported by this provider instance emits nothing further, and an absent-labels set that gains the requested path when the candidate count is zero. Keep the message's two causes distinguishable exactly as they are today. Route every emission through the `ErrorSink` as well as through log4net. In ResolveLeafKeyAsync, remove the requested path from the absent-labels set on BOTH success routes — the exact-path match that returns at lines 74-77 and a successful unique-suffix match — so a label that becomes resolvable after a snapshot refresh is no longer reported absent. Replace the [P1-T4] `IsAbsentLabel` seam with a lookup against the absent-labels set. A bare `HashSet` is prohibited here because ResolveLeafKeyAsync awaits AcquireSnapshotAsync and its continuations are not guaranteed to run on one thread; a static set is prohibited because it is process-wide mutable state shared across viewers and across test methods in one assembly. Acceptance: `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` all pass; `ResolveLeafKeyAsync_ArchiveRelativeStem_ResolvesToUniqueSuffixMatchNode` in UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs still passes; and a case-sensitive search of the provider file returns zero matches for the single-line literal `Issue #799: the absence report body is supplied by`. + +- [ ] [P2-T7] Replace the ProjectSuggestionPath body at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 848-861 with a delegation to `ArchiveStemProjection.ToDisplayStem`, passing the archive root read defensively from the globals so the existing `_globals is null` identity guard is preserved without a separate early return. This eliminates the empty-root one-separator strip that AC4 names, because the contract returns false for a whitespace-only root at ArchiveStemContract.cs:113. Both call sites are display paths and are unchanged: AddSuggestions at line 810 and AddSuggestionRows at line 842. Acceptance: `Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths` and `Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs both still pass, and `AddSuggestions_WhenSuggestionsExist_AppendsHeaderAndTopSuggestions` still passes. + +- [ ] [P2-T8] Project each recent entry through the same helper at both AC5 sites in `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`: the string append at line 793 inside AddRecents, and the row-model mirror at line 879 inside AddRecentRows. Both must be projected, because the XML doc at lines 233-242 asserts that the string list and the row list are text-identical and the row list is the one the breadcrumb surfaces actually consume, so projecting only one would break a documented contract. Acceptance: all four `[TestMethod]` tests in `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` pass, and `AddRecents_WhenRecentsExist_AppendsHeaderAndEntries`, `FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents` and `FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds` all still pass, because their fixtures use already-relative recents for which the projection is the identity. + +- [ ] [P2-T9] Rewrite the include-children TRUE branch of GetOlSubpath at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 955-965 as a verified prefix removal through `ArchiveStemContract.TryMakeArchiveRelative`, falling back to the input path when the contract returns false, so a path that does not start with the ancestor yields a diagnosable value instead of a garbage substring and a path no longer than the ancestor no longer throws ArgumentOutOfRangeException. The contract's parameter is only NAMED archiveRoot and is root-agnostic, which is what lets it serve this site, where the ancestor is a search root supplied by the caller. Leave the include-children FALSE branch at lines 966-970 exactly as it is: it computes a leaf name, which the contract does not do, and converting it would change a different function. Acceptance: `GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs still passes, and both GetRelevantOlPathPortion tests in ToDoModel.Test, directory Email Utilities, file FolderHandlerTests_Written.cs still pass. + +- [ ] [P2-T10] Replace the ProjectPredeterminedFolder body at `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` lines 272-285 with a one-line delegation to `ArchiveStemProjection.ToDisplayStem`, retaining the member itself so its existing test keeps a target, and rewrite the XML doc at lines 252-271 so the two paragraphs describing the empty-root divergence are replaced by a statement that both members now share one projection and that the empty-root strip was removed by AC4. Also rewrite the duplication-rationale comment at lines 223-230, whose stated reason — that FolderPredictor.ProjectSuggestionPath is private and lives under UtilitiesCS, which the earlier change could not modify — becomes false the moment the shared helper is public. Acceptance: `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` passes as retargeted by [P1-T14]; a case-sensitive search of the file returns zero matches for the single-line literal `is private and lives under UtilitiesCS`; and the file is at or below its [P0-T14] baseline of 312 lines. + +- [ ] [P2-T11] Project the score paths inside the internal four-argument BindRowsAsync at `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` lines 92-150, immediately before the `_builder.BuildRows` call at lines 132-136, using the bound root the method already normalizes at lines 107-109. Build a new score list that contains every original score UNCHANGED and, additionally, one projected `FolderScore` carrying `ArchiveStemProjection.ToDisplayStem(score.FolderPath, _boundRoot)` with the same score and probability whenever that projection differs from the original path under an ordinal comparison. The addition is what makes it safe: substitution would fix the stem-presented case and silently break the rooted-presented case (D7), and the probability index at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:224 assigns through the indexer, so duplicate keys are tolerated rather than throwing. The public three-argument overload at lines 75-82 forwards an empty root, so the projection is the identity for every caller of that overload and no existing behaviour changes there. Do not modify the row builder and do not change any public signature; re-keying the join was considered and rejected because BuildRows takes only a string list and a score sequence, so there is no correlating identity to key on without changing a public signature and every test that calls it. Acceptance: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage`, `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` and `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` all pass, and `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` and `Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` in QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs both still pass. + +- [ ] [P2-T12] Apply the AC7 zero-candidate row suppression on the Efc surface only, per the escalation branch D5 records, inside the same internal BindRowsAsync in `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`. Add a private readonly `IFolderLabelAbsenceReport` field assigned in the existing constructor body at lines 48-56 from `provider as IFolderLabelAbsenceReport`, which changes no constructor signature and therefore breaks no existing test; a `Mock` is not an `IFolderLabelAbsenceReport`, so the field is null and suppression is inert in every existing router test (D4). In the chain loop at lines 110-130, when a suggestion row's hierarchy path is non-null, its fetched chain is null, and the absence report says that hierarchy path is an absent label, record the presented text in a suppression set; a null chain arising from cancellation or from a provider fault is NOT suppressed, because those rows are not known-absent. Derive the retained presented-row list from the suppression set and pass that SAME list to both `_builder.BuildRows` at lines 132-136 and `AttachSegmentKeys` at line 137, because AttachSegmentKeys indexes the presented rows by row index at line 176 and would mis-align against an unfiltered list. Log the suppressed count at DEBUG through the `log` field declared at lines 21-23. When nothing is suppressed, pass the original list unchanged so the common path allocates nothing. Acceptance: the solution compiles; every `[TestMethod]` in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` passes; and all five #439 router tests named in [P0-T15] still pass, which is the observable proof that suppression is inert behind a mocked provider. + +- [ ] [P2-T13] Add the lazy root accessor argument to the provider construction at `QuickFiler/Controllers/EfcFormController.cs` lines 1053-1055, passing a delegate that reads the archive root from the existing application-globals accessor at call time rather than at construction time. The delegate form is mandatory: the archive-root property throws when the root is unresolvable, this construction is not inside a try, and QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242 exists because of that throw (D2). Change nothing else in this file; the raw score read at lines 1115-1117 and the four-argument router call at line 1118 stay exactly as they are, because AC6 is delivered in the router (D7) and this file cannot absorb growth. Acceptance: the solution compiles; `BindBreadcrumbRowsAsync_WhenArchiveRootThrows_ReportsOnceAndDoesNotThrow` still passes; and the file is at or below 1322 lines, that is its [P0-T14] baseline of 1320 plus the at-most-two lines the added argument costs (D11). + +- [ ] [P2-T14] Add the same lazy root accessor argument to the provider construction inside the relocated helper in `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task must run after [P2-T3] (R9). Change nothing in `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` in this task. Acceptance: the solution compiles; a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` finds the single-line token `ArchiveRootPath`; and `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is at or below 500 lines. + +- [ ] [P2-T15] Build the solution and record `/evidence/regression-testing/p2-t15-build.md`. Acceptance: `EXIT_CODE: 0`. + +```powershell +msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" +``` + +- [ ] [P2-T16] Run every test named in the [P1-T18] inventory and record `/evidence/regression-testing/p2-t16-pass-after.md`. Acceptance: `EXIT_CODE: 0`, `Failed: 0`, and the artifact records, for every test named in the [P1-T18] inventory, a `PASS-AFTER: ` line derived from the TRX, with the count of those lines equal to the [P1-T18] inventory count. The two runs' own totals are recorded separately as `P2-T16-TOTAL-PASSED:` and `P2-T16-TOTAL-RUN:` and are NOT asserted against the inventory count, because the filters select whole classes and therefore also run tests that were already green at the end of Phase 1. + +```powershell +$vswhere = "${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 +& $vstest UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p2-t16-ut' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~ArchiveStemProjectionTests|FullyQualifiedName~ArchiveChainProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTrimTests|FullyQualifiedName~FolderPredictorRecentsProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTests' +& $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p2-t16-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterScoreJoinTests|FullyQualifiedName~QfcItemController_FolderHandlingTests|FullyQualifiedName~BreadcrumbBridgeRouterIssue439Tests' +``` + +- [ ] [P2-T17] Run the whole UtilitiesCS.Test and QuickFiler.Test assemblies and record `/evidence/regression-testing/p2-t17-suites.md` with the derived lines `POST-UT-TOTAL:`, `POST-UT-PASSED:`, `POST-UT-FAILED:`, `POST-QFT-TOTAL:`, `POST-QFT-PASSED:`, `POST-QFT-FAILED:` and a `NEWLY-FAILING:` line listing every test failing here that was not failing in the [P0-T11] baseline. Acceptance: `NEWLY-FAILING: NONE`, `POST-UT-FAILED` is less than or equal to `BASELINE-UT-FAILED`, and `POST-QFT-FAILED` is less than or equal to `BASELINE-QFT-FAILED`. + +```powershell +$vswhere = "${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 +& $vstest UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p2-t17-ut' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests' +& $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p2-t17-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook' +``` + +- [ ] [P2-T18] Record the pre-format line count of every file this plan has edited or created into `/evidence/qa-gates/p2-t18-sizes-interim.md`, one ` = ` line per file alongside its [P0-T14] baseline count, keeping the four project files under the same `PROJECT-FILE (exempt):` heading [P0-T14] uses (R8). Acceptance: every listed `.cs` count satisfies its D11 budget; the exempt project-file counts are recorded but not asserted against the ceiling; and any `.cs` file within ten lines of its budget is named explicitly with its remaining headroom. + +--- + +### Phase 3 — Final QA loop, coverage, and acceptance-criteria closure + +- [ ] [P3-T1] Run the CSharpier formatter over the repository and record `/evidence/qa-gates/p3-t1-format.md`. `format` rewrites tracked source and still exits 0 after rewriting, so the exit code alone cannot distinguish a clean run from a repairing one; the artifact must therefore record the verbatim printed line of the form `Formatted files in ms.` and, as the distinguishing observation, the `git status --porcelain --untracked-files=all` path set and the `git diff --stat` output anchored to `BASE-SHA`, captured before and after the run, with the two derived lines `PATH_SETS_IDENTICAL:` and `DIFFSTAT_IDENTICAL:`. If the formatter rewrote a file outside this plan's Write Set, that path is recorded and the rewrite is reverted, so the repository-wide pass cannot widen the scope boundary [P3-T11] asserts. Acceptance: `EXIT_CODE: 0` and both derived comparison lines are recorded with their values. + +```powershell +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +$BaseSha = (Select-String -Path 'docs\features\active\2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799\evidence\baseline\p0-t2-base.md' -CaseSensitive -Pattern '^BASE-SHA: ([0-9a-f]{40})$').Matches[0].Groups[1].Value +git add --intent-to-add -- '*.cs' '*.csproj' +$before = @(git status --porcelain --untracked-files=all) +$beforeStat = @(git diff --stat $BaseSha) +dotnet tool run csharpier format . +$after = @(git status --porcelain --untracked-files=all) +$afterStat = @(git diff --stat $BaseSha) +``` + +- [ ] [P3-T2] Run the read-only CSharpier check and record `/evidence/qa-gates/p3-t2-format-check.md` with the verbatim printed line and the derived line `FINAL-CSHARPIER-CHECKED-FILES: `. The success-case output on a clean tree is the single line of the form `Checked files in ms.` with exit 0. Record the delta against `BASELINE-CSHARPIER-CHECKED-FILES` from [P0-T8]; seven new `.cs` files are added by this plan, so a delta of 7 is the expected observation, and any other value must be explained in the artifact. Acceptance: `EXIT_CODE: 0`. The exit code is the gate here, because `check` is read-only and returns non-zero on drift. + +```powershell +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +dotnet tool run csharpier check . +``` + +- [ ] [P3-T3] Run the analyzer gate and record `/evidence/qa-gates/p3-t3-analyzers.md`, comparing its warning and error counts against [P0-T9]. Acceptance: `EXIT_CODE: 0` and the error count is 0. + +```powershell +msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +``` + +- [ ] [P3-T4] Run the nullable gate and record `/evidence/qa-gates/p3-t4-nullable.md`, comparing its warning and error counts against [P0-T10]. `/p:Nullable=enable` must not be added and `/t:Build` must not be substituted. Acceptance: `EXIT_CODE: 0` and the error count is 0. + +```powershell +msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +``` + +- [ ] [P3-T5] Run the full nine-assembly suite under `dotnet-coverage`, writing the Cobertura document to artifacts\csharp\coverage.xml, and record `/evidence/qa-gates/p3-t5-tests-coverage.md` with the derived lines `FINAL-LINES-COVERED:`, `FINAL-LINES-VALID:`, `FINAL-BRANCHES-COVERED:`, `FINAL-BRANCHES-VALID:`, `FINAL-PACKAGES-MATCHED:`, the two derived percentages, and `FINAL-TOTAL-TESTS:` / `FINAL-FAILED-TESTS:`. The four counters are aggregated by the same pinned block [P0-T12] used, over the same nine first-party package names, under the same derived coverage configuration and the same test-case filter, so the two sides are produced by one collector, one configuration, one selection and one filter. The artifacts directory is git-ignored at `.gitignore` line 57, so the document is a local tool output rather than committed evidence; the acceptance below is on-disk existence and the recorded counters, not on `git ls-files`. Acceptance: `EXIT_CODE: 0`, `FINAL-FAILED-TESTS: 0`, artifacts\csharp\coverage.xml exists, all five `FINAL-` counter lines are numeric, and `FINAL-PACKAGES-MATCHED` equals `BASELINE-PACKAGES-MATCHED` from [P0-T12]. + +```powershell +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path +$env:PATH = "$env:DOTNET_ROOT;$env:PATH" +$vswhere = "${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 +New-Item -ItemType Directory -Force -Path 'artifacts\csharp' | Out-Null +dotnet-coverage collect --output artifacts\csharp\coverage.xml --output-format cobertura --settings coverage\799-effective-coverage.config -- $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll SVGControl.Test\bin\Debug\SVGControl.Test.dll Tags.Test\bin\Debug\Tags.Test.dll TaskMaster.Test\bin\Debug\TaskMaster.Test.dll TaskTree.Test\bin\Debug\TaskTree.Test.dll TaskVisualization.Test\bin\Debug\TaskVisualization.Test.dll ToDoModel.Test\bin\Debug\ToDoModel.Test.dll UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll VBFunctions.Test\bin\Debug\VBFunctions.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p3-t5' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests' +$doc = New-Object System.Xml.XmlDocument +$doc.Load((Resolve-Path -LiteralPath 'artifacts\csharp\coverage.xml').Path) +$first = @('QuickFiler','SVGControl','Tags','TaskMaster','TaskTree','TaskVisualization','ToDoModel','UtilitiesCS','VBFunctions') +$lc=0;$lv=0;$bc=0;$bv=0;$pm=0 +foreach ($pkg in $doc.SelectNodes('//package')) { + $name = $pkg.GetAttribute('name') + if ($first -notcontains $name) { continue } + $pm++ + foreach ($ln in $pkg.SelectNodes('.//line')) { + $lv++ + if ([int]$ln.GetAttribute('hits') -gt 0) { $lc++ } + $cc = $ln.GetAttribute('condition-coverage') + if ($cc -match '\((\d+)/(\d+)\)') { $bc += [int]$Matches[1]; $bv += [int]$Matches[2] } + } +} +"LINES_COVERED=$lc LINES_VALID=$lv BRANCHES_COVERED=$bc BRANCHES_VALID=$bv PACKAGES_MATCHED=$pm" +``` + +- [ ] [P3-T6] Record the toolchain loop closure into `/evidence/qa-gates/p3-t6-loop.md`, listing [P3-T1] through [P3-T5] in order with each artifact path and each recorded exit code, and stating whether any step failed or rewrote a file. Acceptance: the artifact records all five steps as passing in one uninterrupted pass, or, if any step failed or changed files, records the restart and the subsequent clean pass; the checklist box for this task stays unchecked until a clean pass is recorded. + +- [ ] [P3-T7] Write `/evidence/qa-gates/p3-t7-changed-lines.md` comparing coverage on the changed production lines. Restrict the comparison to the paths [P0-T13] reported as `MEASURABLE:`; for each path reported `UNMEASURABLE:`, record `CHANGED-LINE-COVERAGE: NOT MEASURABLE` with the reason the determination gave and name the passing tests that exercise those changed lines as the substitute evidence. For each measurable path, derive the changed line numbers from the anchored `git diff --unified=0` in the command block below and record each changed line's `hits` value from artifacts\csharp\coverage.xml, using a de-duplicated per-line map that merges the class-level line elements with the method-level line elements keyed by line number and resolved by maximum `hits`. Where a diff hunk's added and removed line counts are unequal, no one-to-one baseline mapping exists; record such lines as `baseline=none` and exclude them from the regression count rather than attributing borrowed coverage. A changed line carrying no line element in either branch of the merged map is non-executable — an XML doc comment, a blank line, a using directive, a brace or an interface method declaration — and has no `hits` value; record such lines as `hits=non-executable` and exclude them from both the `hits = 0` count and the regression count. Acceptance: every changed production line in the measurable set is recorded with either a post-change `hits` value or the `hits=non-executable` marker; the count of changed lines with `hits = 0` is stated over executable lines only; and the count of changed lines whose post-change `hits` is lower than their baseline `hits` is `0`. + +```powershell +$BaseSha = (Select-String -Path 'docs\features\active\2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799\evidence\baseline\p0-t2-base.md' -CaseSensitive -Pattern '^BASE-SHA: ([0-9a-f]{40})$').Matches[0].Groups[1].Value +git add --intent-to-add -- '*.cs' +git status --porcelain --untracked-files=all -- 'UtilitiesCS/OutlookObjects/Folder' 'QuickFiler/Controllers' +foreach ($p in @('UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs', 'UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs', 'QuickFiler/Controllers/QfcItemController.FolderHandling.cs', 'QuickFiler/Controllers/BreadcrumbBridgeRouter.cs', 'QuickFiler/Controllers/EfcFormController.cs', 'QuickFiler/Controllers/QfcItemController.ViewerSetup.cs')) { + "=== $p" + git diff --unified=0 $BaseSha -- $p +} +``` + +- [ ] [P3-T8] Write `/evidence/qa-gates/p3-t8-coverage-delta.md` comparing the five [P0-T12] baseline counters against the five [P3-T5] final counters. Record the comparability precondition first: `FINAL-LINES-VALID` and `BASELINE-LINES-VALID` must be compared and their relation stated, because the denominator grows when new production lines are added and the two sides are only directly comparable when it does not. When the denominators differ, compare the two derived percentages instead and state that the percentage comparison is the one used. Acceptance: the artifact records baseline coverage, post-change coverage, and the changed-line determination from [P3-T7], and states explicitly whether the repository-wide first-party line percentage decreased. + +- [ ] [P3-T9] Write `/evidence/qa-gates/p3-t9-new-type-coverage.md` recording line and branch coverage for the two new production types, read from artifacts\csharp\coverage.xml by selecting the class elements whose `filename` attribute ends with a directory separator followed by `ArchiveStemProjection.cs` or `ArchiveChainProjection.cs`. The repository unit-test policy requires new modules, classes and methods to target at least 90 percent coverage. Acceptance: the artifact records a numeric line-coverage percentage for each of the two files, both are at or above 90 percent, and the class-element counts the figures were derived from are recorded so a zero-element selection is visible rather than silent. + +- [ ] [P3-T10] Record the post-format line count of every file this plan edited or created into `/evidence/qa-gates/p3-t10-sizes.md`, one ` = ` line per file alongside its [P0-T14] baseline, keeping the four project files under the same `PROJECT-FILE (exempt):` heading (R8). This audit runs AFTER the final format because CSharpier can change line counts (R9). Acceptance: `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is at or below 500; `QuickFiler/Controllers/EfcFormController.cs` is at or below 1322; `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` is at or below 1003; every other listed `.cs` file is at or below 500; the exempt project-file counts are recorded but not asserted against the ceiling; and the artifact states the smallest remaining headroom across all listed `.cs` files together with the three disclosed pre-existing over-ceiling files and their budgets (D11). + +- [ ] [P3-T11] Write `/evidence/qa-gates/p3-t11-scope.md` enumerating the changed source set under the R7 pathspec and asserting the scope boundary. The artifact must list the anchored-diff output and the porcelain output side by side, because neither alone is correct in both states: an anchored diff cannot see an untracked path, and porcelain status goes empty once the change is committed. Acceptance: the enumerated set contains only the twenty Write Set paths this plan actually writes — the nine production paths, the five new test paths, the two retargeted test paths, and the four project files — and contains none of the six sibling-owned files named in D1, which are, in bare prose, UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs, UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.SearchPresentation.cs, UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs, UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.Highlight.cs, QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs and QuickFiler/Viewers/BreadcrumbBridgeCoordinator.Search.cs; and contains none of QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs, QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs, UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs, UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs, UtilitiesCS/OutlookObjects/Folder/FolderTreeSnapshotQueries.cs, UtilitiesCS/OutlookObjects/Folder/IFolderHierarchyProvider.cs, UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs, QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs or QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs, the last two per the D8 no-hunk finding. + +```powershell +$BaseSha = (Select-String -Path 'docs\features\active\2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799\evidence\baseline\p0-t2-base.md' -CaseSensitive -Pattern '^BASE-SHA: ([0-9a-f]{40})$').Matches[0].Groups[1].Value +git add --intent-to-add -- '*.cs' '*.csproj' +git diff --name-only $BaseSha -- '*.cs' '*.csproj' +git status --porcelain --untracked-files=all -- '*.cs' '*.csproj' +``` + +- [ ] [P3-T12] Write `/evidence/qa-gates/p3-t12-ac8-verification.md` recording the AC8 finding as verified evidence rather than as a fix, per `spec.md` decision D-C. The artifact must re-run and record the repository-wide negative search below over the six product projects, and must record the five traced transformations and why each leaves a leading underscore unchanged: the verbatim splitter splits on path separators with empty entries removed and inserts nothing; the JSON serializer escapes only the double quote, the backslash and control characters, and the non-indenting format adds no whitespace inside string values; the QuickFiler page assigns segment text through the DOM textContent property, which performs no entity decoding and no transformation; the Efc page encodes ampersand, less-than, greater-than and double-quote only, so underscore and space pass through and no non-breaking space is emitted; and neither stylesheet contains letter-spacing, word-spacing, text-transform, a first-letter pseudo-element or word-break. It must also record that the WinForms mnemonic prefix character is the ampersand and not the underscore, so no combo-box or owner-draw path can be responsible. The conclusion is recorded as a definite finding: the reported space after the leading underscore was a transcription artifact, the renderer is correct, and a renderer change would be a defect. No file is edited by this task. Acceptance: the search records zero matching files, the artifact states the conclusion as a definite finding, and the artifact records `FILES-CHANGED-FOR-AC8: 0`. + +```powershell +git grep -n -E "Replace\(\"_|Replace\('_|letter-spacing|text-transform|first-letter|word-break|word-spacing" -- "UtilitiesCS/*.cs" "UtilitiesCS/*.html" "UtilitiesCS/*.css" "QuickFiler/*.cs" "QuickFiler/*.html" "QuickFiler/*.css" "ToDoModel/*.cs" "TaskMaster/*.cs" "Tags/*.cs" "TaskVisualization/*.cs" +``` + +- [ ] [P3-T13] Check off AC1 in `spec.md` line 834 by changing its `- [ ]` to `- [x]`, citing the [P1-T16] fail-before artifact and the [P2-T16] pass-after artifact and naming the passing tests `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` and `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`. Acceptance: exactly one AC checkbox changes in this task and the AC1 line carries `- [x]`. + +- [ ] [P3-T14] Check off AC2 in `spec.md` line 835, citing [P2-T16] and naming the passing tests `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty` and `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty`, and recording that AC2's phrase "existing single-segment fallback" is literally true only on the Efc surface, where the row builder's empty-chain branch renders one leaf-only segment, while the QuickFiler surface routes non-suggestion rows through the verbatim splitter and therefore renders a multi-level stem as several segments — existing behaviour that already satisfies AC2's substantive requirement that no row shows a mailbox prefix. Acceptance: exactly one AC checkbox changes in this task and the AC2 line carries `- [x]`. + +- [ ] [P3-T15] Check off AC3 in `spec.md` line 836, citing the passing `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` from [P2-T16] and recording that this is an explicit pin rather than an incidental consequence, together with the structural reason: the trim removes only LEADING segments while the filing value is substituted into the LEAF segment. Acceptance: exactly one AC checkbox changes in this task and the AC3 line carries `- [x]`. + +- [ ] [P3-T16] Check off AC4 in `spec.md` line 837, citing [P1-T17] and [P2-T16] and the passing retargeted `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection`, and recording the D-A site disposition: four sites converted ([P2-T7], [P2-T8], [P2-T9], [P2-T10]) and three deliberately left with the reasons D10 records. Acceptance: exactly one AC checkbox changes in this task and the AC4 line carries `- [x]`. + +- [ ] [P3-T17] Check off AC5 in `spec.md` line 838, citing the four passing tests in `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` from [P2-T16] and recording that both the string append and the row-model mirror are projected, because the row list is the one the breadcrumb surfaces consume and the documented text-parity contract would otherwise break. Acceptance: exactly one AC checkbox changes in this task and the AC5 line carries `- [x]`. + +- [ ] [P3-T18] Check off AC6 in `spec.md` line 839, citing [P1-T17] and [P2-T16] and the passing `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, and recording the two deviations from the spec's own wording: the projection is applied in the Efc router rather than at the controller call site named by the criterion, because the controller is 1320 lines and cannot absorb growth while the router already normalizes the bound root; and the projected score is ADDED alongside the raw score rather than substituted for it, per D7. Acceptance: exactly one AC checkbox changes in this task and the AC6 line carries `- [x]`. + +- [ ] [P3-T19] Check off AC7 in `spec.md` line 840, citing the passing `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` from [P2-T16], and recording three things: that the filtered branch is taken restricted to the zero-candidate case per decision D-B; that "per session" is realized as "per provider instance" and is enforced by a thread-safe per-instance set rather than a static one; and that the escalation branch D5 records was taken, so row suppression is delivered on the Efc surface only while the logging half is delivered on both surfaces. Acceptance: exactly one AC checkbox changes in this task, the AC7 line carries `- [x]`, and the check-off note names the escalation deviation. + +- [ ] [P3-T20] Check off AC8 in `spec.md` line 841, citing `/evidence/qa-gates/p3-t12-ac8-verification.md` and recording that AC8 is satisfied by the verified finding that the renderer does not alter a leading underscore, that no code change was made, and that a renderer change would have been a defect. Acceptance: exactly one AC checkbox changes in this task, the AC8 line carries `- [x]`, and no file under `QuickFiler/Resources` appears in the [P3-T11] scope enumeration. + +- [ ] [P3-T21] Update the `spec.md` Status line to `Implemented` and add an "Outcome" note under Rollout & Follow-up recording the four deviations this plan makes from the spec's own prose, each with its reason: AC7 row suppression is delivered on the Efc surface only, because the QuickFiler presented row set is composed solely inside the sibling-owned bridge router, which decision D-B forbids this item from editing (D5); the AC6 score projection is additive rather than substitutive, so the rooted-presented-text join cannot regress (D7); the two #439 Efc router test files listed in the Write Set carry no hunk, because every test in both drives a mocked provider below the trim boundary and editing their shared chain fixture would break unrelated #614 boundary tests (D8); and the AC7 absence classification is published through a new small public interface declared in the provider's own file rather than through a fourth member on the shared hierarchy contract, which net48 cannot add without breaking every implementer and every strict mock (D4). Acceptance: the Status line reads `Implemented` and all four deviations are recorded by name. + +- [ ] [P3-T22] Update `issue.md` with the outcome and mirror it to `/evidence/issue-updates/issue-799..md` per the evidence conventions, including the literal field lines `Timestamp:`, the exact text intended, and `PostedAs:`. The update must state that this is a specification change superseding issue #439's full root-to-leaf lineage, not a regression fix against it, and that #439's filing-target and score-key constraint is preserved and carried forward as AC3. Acceptance: both the local `issue.md` update and the mirror artifact exist and carry the same text. + +- [ ] [P3-T23] Write `/evidence/qa-gates/p3-t23-ac-summary.md` listing AC1 through AC8 with their final checkbox state and the artifact path that justifies each. Acceptance: eight rows are present, each naming at least one existing artifact path, and every row's checkbox state matches the corresponding line in `spec.md`. + +--- + +SELF-REVIEW: RE-DERIVED THIS PASS + +Round 1 (initial authoring). Every citation in the Citation table was read directly from this worktree in this pass, and +the sibling lines, tests and fixtures in the same region as each edited citation were re-checked. The sibling sweep produced +seven findings that changed the plan away from what the spec and the research alone would have produced: + +- D5, the escalation branch. FolderBreadcrumbBridgeRouter.SetSuggestionsAsync composes the QuickFiler presented row set as + the local list at lines 42-86 of the sibling-owned file, and QfcItemController.FolderHandling.cs only hands the predictor + row model to the viewer at lines 212 and 221, before any resolution has happened. Spec decision D-B's escalation + condition is therefore met and its documented fallback is taken. +- D7, the additive score projection. BreadcrumbRowBuilder.BuildProbabilityIndex assigns through the indexer at line 224, + so duplicates are safe, and Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively at lines 118-166 binds + a rooted presented row together with a rooted score. A substituting projection would have silently removed that row's + percentage without failing any test. +- D8, the two #439 test files need no hunk. Every test in both files uses a strict Moq provider, and the shared Chain + helper's leading Archive segment at line 444 is load-bearing for Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection, + which asserts at line 426 that activating segment 0 yields the archive root. +- D9, the retargeting surface is two tests, and the reason each other candidate family stays green was derived rather than + assumed: the recents fixtures use the relative value Recent\One, the Issue609 tests already encode ToDisplayStem's exact + semantics, and both GetOlSubpath fixtures pass a separator-terminated proper prefix. +- D4, the separate diagnostics interface. IFolderHierarchyProvider declares exactly three members, has exactly one concrete + implementer, and is mocked with MockBehavior.Strict in every router test, so a fourth member would have thrown at the + first production call inside those tests. +- D2, the optional constructor parameter binds unambiguously. The provider declares exactly one constructor, so + new OutlookFolderHierarchyProvider(null) at OutlookFolderHierarchyProviderTests.cs:316 still binds to the first + parameter; 19 existing constructions across two test files were counted line by line. +- D11 and R9, the file-size budgets. Line counts were re-measured in this worktree: QfcItemController.ViewerSetup.cs is + exactly 500, EfcFormController.cs is 1320 and legitimately grows by the added argument so a no-growth gate on it would be + unsatisfiable, FolderPredictor.cs is 1003 and shrinks by at least eight lines by derivation, and + OutlookFolderHierarchyProviderTests.cs is 479 with only 21 lines of headroom for its retarget. + +Delta self-check: this document's own prose was checked against the rules it enforces. Every backticked path with a +directory separator is a Write Set path taken from spec.md lines 645-702; every sibling-owned, precedent, comparison and +out-of-scope path is written in bare prose; both space-containing paths are named in words only and neither is in this +item's footprint; no asserted token carries an angle bracket, a dollar-brace, a dollar-paren or a percent sign; every +`git diff` carries an explicit ref operand bound inside its own block together with a staging or porcelain companion; the +two write-mode commands (the formatter and the repo-local SDK installer) each record an observation beyond their exit code; +the coverage aggregation prints the exact line its acceptance reads; and no task's acceptance depends on an artifact a +later task writes — [P0-T13] and [P0-T14] precede every consumer, [P1-T18] precedes [P2-T16], [P0-T12] precedes [P3-T8], +and [P3-T5] precedes [P3-T7], [P3-T8] and [P3-T9]. + +PLANNER-INTERNAL-REVIEW: PASS +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS +CITATION: UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs | 147 lines; TryMakeArchiveRelative at lines 106-145; whitespace-root guard at line 113; trailing-separator trim at line 118; equality returning true with an empty stem at lines 124-127; StartsWith prefix test at line 131; separator-boundary test at lines 137-141; stem leading-separator trim at line 143 +CITATION: UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs | 141 lines; log4net ILog at lines 17-19; single constructor at lines 28-31; GetAncestorChainAsync at lines 34-42; ResolveLeafKeyAsync at lines 56-80 with the exact-path early return at lines 74-77; private static ResolveByUniqueSuffix at lines 90-114 with the two-cause emission at lines 108-112 +CITATION: UtilitiesCS/OutlookObjects/Folder/IFolderHierarchyProvider.cs | 65 lines; exactly three members at lines 31-34, 46-49 and 60-63 +CITATION: UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs | 1003 lines; text-parity XML doc at lines 233-242; AddRecents at lines 788-795 with the unprojected AddRange at line 793; AddSuggestions at lines 807-811; AddSuggestionRows at lines 835-846; ProjectSuggestionPath at lines 848-861 with the length guard at line 858; AddRecentRows at lines 866-882 with the unprojected row at line 879; GetOlSubpath at lines 953-971 with the include-children true branch at lines 955-965 +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs | 238 lines; empty-chain single-segment fallback at lines 123-131; presented-text probability lookup at lines 133-135; MapSegments at lines 178-208; BuildProbabilityIndex at lines 210-229 assigning through the indexer at line 224 +CITATION: UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbSegment.cs | 54 lines; four-argument constructor at lines 29-40; FolderPath property at line 49 +CITATION: UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs | 489 lines, sibling-owned; SetSuggestionsAsync at lines 29-97 composing the presented row set at lines 42-86 and swapping it under the shared lock at lines 88-96; shared _sync at line 15 and _suggestionGeneration at line 16 +CITATION: UtilitiesCS/Properties/AssemblyInfo.cs | InternalsVisibleTo("UtilitiesCS.Test") at line 19 +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.cs | 304 lines; log field at lines 21-23; _boundRoot at line 35; constructor at lines 41-56; public three-argument BindRowsAsync at lines 75-82 forwarding an empty root at line 81; internal four-argument BindRowsAsync at lines 92-150 with root normalization at lines 107-109, chain loop at lines 110-130, BuildRows call at lines 132-136 and AttachSegmentKeys call at line 137; ToHierarchyPath at lines 152-167; AttachSegmentKeys indexing presented rows at line 176 +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | 221 lines; FetchChainAsync at lines 50-81 returning null on a null key at lines 61-64 and on both catch arms at lines 68-80 +CITATION: QuickFiler/Controllers/EfcFormController.cs | 1320 lines; ConfigureBreadcrumbControl at lines 1047-1067 with the provider construction at lines 1053-1055; BindBreadcrumbRowsAsync at lines 1111-1128 with the raw score read at lines 1115-1117 and the four-argument router call at line 1118 +CITATION: QuickFiler/Controllers/QfcItemController.ViewerSetup.cs | exactly 500 lines; usings at lines 1-22; internal partial class at line 26; EnsureBreadcrumbPipeline comment at lines 132-136, ExcludeFromCodeCoverage attribute at line 137, member at lines 138-163, provider construction at lines 147-149 +CITATION: QuickFiler/Controllers/QfcItemController.FolderHandling.cs | 312 lines; AssignFolderComboBox at lines 191-250 with the row-model hand-off at lines 212 and 221 and the projection call at lines 231-234; duplication-rationale comment at lines 223-230; ProjectPredeterminedFolder XML doc at lines 252-271 and body at lines 272-285 +CITATION: UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs | 479 lines; namespace at line 11; GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments at lines 72-91 with its root-to-leaf assertion at lines 84-87; 13 provider constructions at lines 76, 98, 118, 139, 157, 175, 193, 218, 240, 269, 299, 316 and 338; single-null-argument construction at line 316 +CITATION: UtilitiesCS.Test/OutlookObjects/Folder/FolderHierarchyProviderAdapterTests.cs | 258 lines; 6 provider constructions at lines 98, 122, 142, 170, 185 and 202 +CITATION: UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs | 1066 lines, pre-existing over-ceiling and untouched; recents test at lines 249-265 with relative recents at line 255; FolderArray recents test at lines 165-189 with the relative recent at line 175; Issue609 projection tests at lines 191-247; GetOlSubpath assertions at lines 577-591 +CITATION: UtilitiesCS.Test/OutlookObjects/Folder/FolderRowTests.cs | text-parity test at lines 29-70 with the relative recent at line 39; recents mock helper at lines 246-258 +CITATION: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs | 455 lines; strict provider mocks at lines 30, 125, 261, 311 and 385; lineage test at lines 20-116 with the archive-root index assertions at lines 109 and 113; rooted-target test at lines 118-166 binding a rooted score at line 149; boundary test at lines 302-377; slash-only-root test at lines 379-427 asserting the archive root at line 426; shared Chain helper at lines 434-448 emitting the leading Archive segment at line 444 +CITATION: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs | 253 lines; strict provider mocks at lines 21, 70, 118 and 186; four uses of the shared Chain helper at lines 33, 82, 130 and 196 +CITATION: QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs | 354 lines; namespace at line 10; partial class QfcItemController_FolderHandlingTests at line 21; ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection at lines 212-243 with the empty-root one-separator assertion at lines 219-226 +CITATION: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs | 358 lines; namespace at line 16; rooted/relative pair bind at lines 254-259 +CITATION: ToDoModel.Test/Email Utilities/FolderHandlerTests_Written.cs | GetOlSubpath assertions at lines 35-50 and 52-67, both passing a separator-terminated proper prefix +CITATION: UtilitiesCS/UtilitiesCS.csproj | ArchiveStemContract.cs Compile Include at line 623; OutlookFolderHierarchyProvider.cs at line 640; FolderPredictor.cs at line 808; EnsureNuGetPackageBuildImports Error at line 1293; Analyzer Include block at lines 1301-1310 naming Meziantou.Analyzer 3.0.203 and Roslynator.Analyzers 5.0.0 +CITATION: UtilitiesCS.Test/UtilitiesCS.Test.csproj | folder-test Compile Include block at lines 276-307 with OutlookFolderHierarchyProviderTests.cs at line 304; EnsureNuGetPackageBuildImports Error at line 946 +CITATION: QuickFiler/QuickFiler.csproj | BreadcrumbBridgeRouter.cs Compile Include at line 291; QfcItemController.ViewerSetup.cs at line 335; QfcItemController.FolderHandling.cs at line 338; EnsureNuGetPackageBuildImports Error at line 586 +CITATION: QuickFiler.Test/QuickFiler.Test.csproj | BreadcrumbBridgeRouterIssue439Tests.cs Compile Include at line 64; QfcItemController.FolderHandlingTests.Part2.cs at line 182; EnsureNuGetPackageBuildImports Error at line 501 +CITATION: global.json | SDK 8.0.205 at line 3; paths ".dotnet-sdk" and "$host$" at lines 6-9; repo-local install-script error message at line 10 +CITATION: .csharpierignore | evidence exclusion at line 4; cobertura at line 5; trx at line 8; project-file exclusion rationale at lines 9-14 +CITATION: .gitignore | test-results bracket class at line 39; artifacts/ at line 57; coverage/* at line 144 +CITATION: coverage.config | ModulePaths Exclude block at lines 12-22 carrying no Test.dll entry +CITATION: .claude/hooks/enforce-evidence-locations.ps1 | artifacts/csharp/ named as permitted at line 26; forbidden prefixes at lines 64-77 +CITATION: scripts/vscode/Install-RepoDotNetSdk.ps1 | present in this worktree; named by the global.json error message as the repo-local SDK installer +CITATION: docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/spec.md | Acceptance Criteria AC1 through AC8 at lines 834-841; Write Set at lines 645-725; decision D-A at lines 283-338; decision D-B at lines 340-383 with the escalation rule at lines 366-374; decision D-C at lines 385-407; decision D-D at lines 409-446; Test Strategy at lines 759-830 +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8 +AC-MAPPING: AC1 | IMPLEMENTATION: P2-T2, P2-T5, P2-T13, P2-T14 | TESTS: P1-T7, P1-T8, P1-T13 | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p2-t16-pass-after.md +AC-MAPPING: AC2 | IMPLEMENTATION: P2-T5 | TESTS: P1-T8 | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p2-t16-pass-after.md +AC-MAPPING: AC3 | IMPLEMENTATION: P2-T5, P2-T11 | TESTS: P1-T11 | EVIDENCE: /evidence/regression-testing/p2-t16-pass-after.md +AC-MAPPING: AC4 | IMPLEMENTATION: P2-T1, P2-T7, P2-T9, P2-T10 | TESTS: P1-T6, P1-T14 | EVIDENCE: /evidence/regression-testing/p1-t17-qft-fail.md and /evidence/regression-testing/p2-t16-pass-after.md +AC-MAPPING: AC5 | IMPLEMENTATION: P2-T8 | TESTS: P1-T9 | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p2-t16-pass-after.md +AC-MAPPING: AC6 | IMPLEMENTATION: P2-T11 | TESTS: P1-T11 | EVIDENCE: /evidence/regression-testing/p1-t17-qft-fail.md and /evidence/regression-testing/p2-t16-pass-after.md +AC-MAPPING: AC7 | IMPLEMENTATION: P2-T6, P2-T12 | TESTS: P1-T8 | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p2-t16-pass-after.md +AC-MAPPING: AC8 | IMPLEMENTATION: P3-T12 | TESTS: P3-T12 | EVIDENCE: /evidence/qa-gates/p3-t12-ac8-verification.md +UNRESOLVED-GAPS: NONE +DIRECTIVE: PREFLIGHT VALIDATION ONLY +PREFLIGHT: REQUESTED — validation-only preflight has NOT been run by this planner, because no atomic-executor delegation tool and no MCP plan validator are present in this planner's tool surface. The orchestrator must obtain one of the two exact signals, `PREFLIGHT: ALL CLEAR` or `PREFLIGHT: REVISIONS REQUIRED`, and a passing `mcp__drm-copilot__validate_orchestration_artifacts` run with `artifact_type: "plan"` before execution begins. This plan is not self-approved. +CONVERGENCE: FURTHER ROUNDS LIKELY — three decisions in this plan depart from the spec's literal wording and a reviewer may want them re-argued or re-scoped: the D5 escalation to Efc-only row suppression, the D8 finding that the two #439 Efc router test files require no hunk, and the D7 additive rather than substitutive score projection. Each is recorded with its re-derived evidence and its consequence, but each is a judgment the reviewer is entitled to reverse, and reversing any of them changes task text rather than plan structure. From 9e6c9307487648cd429786fdf60f0cd53be7d4c0 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sun, 6 Sep 2026 23:24:06 -0400 Subject: [PATCH 04/13] docs(799): record the adjudicated preflight round 1 delta Preflight returned revisions required with twelve blocking findings. The reviewer could not execute any command, because the pre-tool guard refuses pwsh inside an isolated agent worktree, so its runtime claims were adjudicated against the committed evidence of the completed issue 791 run on this same host. Confirmed by that evidence: vstest prints no failed-count line on a fully passing run, so any acceptance condition reading one from console output is unsatisfiable. Refuted by that evidence: the claim that full-framework MSBuild is absent from the path, which a recorded bare invocation contradicts. Narrowed: the execution-environment finding, whose proposed substitute installer was rejected as scope creep. Co-Authored-By: Claude Fable 5.1 --- .../evidence/other/preflight-r1-delta.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/other/preflight-r1-delta.md diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/other/preflight-r1-delta.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/other/preflight-r1-delta.md new file mode 100644 index 000000000..dd57b4bf4 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/other/preflight-r1-delta.md @@ -0,0 +1,99 @@ +# Preflight round 1 delta, adjudicated by the orchestrator + +Timestamp: 2026-09-06T23-40 + +Reviewer signal: `PREFLIGHT: REVISIONS REQUIRED` with `CONVERGENCE: FURTHER ROUNDS LIKELY`. + +## How to use this document + +Apply each ACCEPTED item using the reviewer's verbatim replacement text from the preflight report, +reproduced in the delegation prompt. For every item, report one disposition: +`applied-verbatim`, `applied-with-mechanical-reassembly`, or `not-applied-with-reason`. +Do not silently substitute your own wording. If you judge an accepted item wrong, leave it unapplied +and report the disagreement for the orchestrator to adjudicate. + +## Orchestrator adjudication summary + +The reviewer stated plainly that it could not execute a single command: the PreToolUse guard refuses +every `pwsh` invocation inside this isolated agent worktree. Its claims about runtime behaviour are +therefore reasoning rather than observation. The orchestrator adjudicated each such claim against the +committed evidence of the completed issue #791 run, which contains real recorded invocations on this +same host. + +### ACCEPTED, and confirmed by recorded observation + +- B5 and B6, and M4. On a fully passing run `vstest.console.exe` prints + `Test Run Successful. Total tests: 1339, Passed: 1339, Total time: 13.2586 Seconds.` and prints no + `Failed:` line at all. Source: the #791 baseline artifact `p0-t10-quickfiler-tests.md` lines 14-22, + which itself derives its failed count from the TRX `ResultSummary/Counters` element rather than + from the console. Any acceptance condition in this plan that reads `Failed: 0` from console output + is unsatisfiable on a green run. Apply the reviewer's replacements in full, including the + `EXIT-CODE-UT:` and `EXIT-CODE-QFT:` split with a single roll-up `EXIT_CODE:` field. + +### ACCEPTED on reading, verified independently by the orchestrator + +- B2 and B3. Nullable annotations. The three named files do open with `#nullable enable`, and the + nullable gate runs with warnings as errors, so the declared seams must carry `?`. +- B4. Author the new QuickFiler.Test file in C# 7.3-compatible syntax. The orchestrator verified that + `QuickFiler.Test/QuickFiler.Test.csproj` declares no `LangVersion` while + `UtilitiesCS.Test/UtilitiesCS.Test.csproj` declares `Latest` at its line 18, that no `.cs` file in + QuickFiler.Test carries a `#nullable enable` directive, and that 25 files in UtilitiesCS.Test do. + The two apparent modern constructs in QuickFiler.Test are inside comments and compile nothing. + Whether or not the compiler default is literally 7.3, authoring the new file in the conservative + syntax costs nothing and removes a real build risk, so this is applied as a precaution. +- B7. The double-quoted PowerShell pattern does not parse, and zero matches is this task's success + outcome, so the artifact needs `ExpectedExitCode: 1`. The reviewer observed the exit code directly + through an allowed git invocation. +- B8. The command block assigns four variables and never derives or prints the two values its + acceptance reads. +- B11. The AC7 row-suppression branch is delivered by a task but executed by no test, so it would + ship at zero hits and be checked off on the strength of provider-level tests that cannot reach it. + This is the most consequential finding in the report and it is an acceptance-criterion delivery + gap, not a style issue. +- B12. The 21-line headroom is not achievable, and the later ceiling gate then has no remedy. +- M1, M2, M3, M5. Counting and census errors, each checkable by reading. +- M6, M7, M8, m1, m2, m3, m4. Apply as written. + +### REJECTED, refuted by recorded observation + +- B9. The claim that full-framework MSBuild is not on PATH, and the consequent rewrite of eight + tasks to resolve and invoke a resolved MSBuild path, is refuted. The #791 baseline artifact + `p0-t3-nuget-restore.md` records the command + `msbuild TaskMaster.sln /t:Restore /m /p:RestorePackagesConfig=true /p:Configuration=Debug "/p:Platform=Any CPU"` + with `EXIT_CODE: 0` on this host, invoked as a bare command name, and the #791 plan's later gate + builds use the same bare form and are recorded complete. Do NOT apply B9. Keep the bare `msbuild` + invocations. The reviewer could not test PATH and asserted an unverifiable negative. + +### NARROWED + +- B1. The refusal of `pwsh` is a property of the isolated agent-worktree sandbox the reviewer ran in, + not a property of this plan. Execution of this plan happens later, in the execution phase, and the + identical command shapes are recorded as successfully executed in the #791 run. Do NOT add the + proposed POSIX download-and-extract substitute for the repository SDK installer: it hard-codes a + download URL, duplicates a maintained script, and widens scope. Apply only this narrowed clause, + which records the requirement and forbids silent substitution: + + ```markdown + **R11b — Execution-environment clause.** Every command block in this plan is a PowerShell block and + requires a session in which `pwsh` may be invoked. A worktree-isolated agent session refuses every + Bash invocation of `pwsh`, in both the `-Command` and the `-File` form. The executor records, in + `/evidence/baseline/p0-t3-sdk.md`, the derived line `EXEC-ENVIRONMENT: pwsh-permitted` once + it has confirmed a PowerShell block runs. An executor that cannot obtain such a session reports + BLOCKED at that task and stops; it must not substitute an unrecorded command shape for a documented + one, because a substituted shape is unreviewed and its success-case output is unobserved. + ``` + +- B10. The claim that the global-tool directory must be prepended to PATH is unproven here: the #791 + artifact `p0-t5-dotnet-coverage.md` records `dotnet-coverage --version` exiting 0 with the + probe-only branch taken, so the tool resolves on this host without any PATH amendment. Apply only + the `Get-Command` probe correction, which is sound on its own terms because an unresolvable command + name raises a terminating error rather than setting an exit code, so the existing branch condition + reads a value that does not exist. Do not assert in the plan that the PATH amendment is required; + state it as a conditional applied only when the install branch is taken. + +## Reviewer determinations the orchestrator confirms and does not reopen + +D5, D7 and D8 were each checked by the reviewer against the tree and confirmed. The AC7 zero-candidate +restriction, the AC8 verification-only disposition, the relocation ordering that keeps the 500-line +file within its ceiling, and the absence of any unsatisfiable no-growth gate were all verified. The +ordering-integrity sweep across all 74 tasks found no violation and no task body swap is required. From 1f1f819f643638ec0512a12f12d603807de382b0 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sun, 6 Sep 2026 23:37:43 -0400 Subject: [PATCH 05/13] docs(799): apply the adjudicated preflight round 1 delta to the plan Applies every accepted finding, applies the two narrowed findings in their narrowed form, and leaves the refuted MSBuild finding unapplied. The most consequential change closes an acceptance-criterion delivery gap: the row-suppression branch for criterion 7 was executed by no test and would have shipped uncovered while being checked off on the strength of provider-level tests that cannot reach it. Two named tests now pin the zero-candidate suppression and the ambiguous-case non-suppression. Three assertions that read a failed-count line from console output are corrected to read the test result file instead, because the runner prints no such line on a fully passing run. Declarations added to a nullable-enabled file now carry the annotations the warnings-as-errors gate requires, and the four consuming tasks gained the guards that follow. The new test file is constrained to the older language version its project compiles at. The plan validator now passes with no warnings. Co-Authored-By: Claude Fable 5.1 --- .../plan.2026-09-06T22-01.md | 171 +++++++++++++----- 1 file changed, 122 insertions(+), 49 deletions(-) diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md index 28ffdcc8b..b28d28b86 100644 --- a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md @@ -4,8 +4,8 @@ - **Parent (optional):** none - **Owner:** drmoisan - **Last Updated:** 2026-09-06T22-01 -- **Status:** Ready for preflight -- **Version:** 1.0 +- **Status:** Ready for preflight — revision round 2 (orchestrator-adjudicated round 1 delta applied in place) +- **Version:** 1.2 - **Work Mode:** full-bug (resolved from `issue.md` line 12 and `spec.md` line 9) - **Language in scope:** C# only (UtilitiesCS, QuickFiler, UtilitiesCS.Test, QuickFiler.Test; four legacy non-SDK projects with explicit Compile Include items and no globbing) - **Authoritative AC source:** `spec.md`, section "Acceptance Criteria", lines 834-841, AC1 through AC8. There is no user-story.md in this feature folder and none is required. @@ -106,6 +106,14 @@ recorded a malformed value. An executor that prefers to paste the recorded 40-he run satisfies this rule equally. Each affected task's `Output Summary:` records the resolved vstest path reduced per R3 and the `$BaseSha` value it bound. +**R11b — Execution-environment clause.** Every command block in this plan is a PowerShell block and +requires a session in which `pwsh` may be invoked. A worktree-isolated agent session refuses every +Bash invocation of `pwsh`, in both the `-Command` and the `-File` form. The executor records, in +`/evidence/baseline/p0-t3-sdk.md`, the derived line `EXEC-ENVIRONMENT: pwsh-permitted` once +it has confirmed a PowerShell block runs. An executor that cannot obtain such a session reports +BLOCKED at that task and stops; it must not substitute an unrecorded command shape for a documented +one, because a substituted shape is unreviewed and its success-case output is unobserved. + **R12 — Path-notation rule for this document.** Backticks in this plan are reserved for (a) the concrete repository paths this change creates or modifies, taken from the `spec.md` Write Set and written with forward slashes; (b) bare filenames carrying no directory separator, which a path extractor cannot classify as repository paths; and (c) code identifiers. @@ -209,7 +217,7 @@ rooted, and the percentage would vanish. That test does not assert the percentag `spec.md` lists `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` and `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` as MODIFY/retarget. Re-derived in this pass: every test in both files constructs `new Mock(MockBehavior.Strict)` and supplies the ancestor -chain directly through `ReturnsAsync` (base file lines 30, 125, 261, 311, 385; Activation partial lines 21, 70, 118, 186). +chain directly through `ReturnsAsync` (base file lines 30, 125, 178, 261, 311, 385 — six strict provider mocks, one per test in that file; Activation partial lines 21, 70, 118, 186). The AC1/AC2 trim lives inside the provider's GetAncestorChainAsync, below that mock boundary, so it cannot reach either file. What they pin is the router's faithful rendering of whatever chain the provider returns plus the #614 selection boundaries, both of which this change preserves and both of which are the AC3 invariant the spec carries forward. @@ -226,7 +234,10 @@ The retarget that IS load-bearing is the one that drives the real provider — s asserts the full store-rooted chain `\Root`, `\Root\Clients`, `\Root\Clients\Acme` at lines 84-87. It would stay green untouched only because it constructs the provider WITHOUT a root accessor, which is not how production constructs it; leaving it that way would pin only the disabled configuration. [P1-T13] rewrites it against a provider configured with a -root accessor, as production configures it, and keeps a companion case for the no-accessor construction. +root accessor, as production configures it. The no-accessor companion case is NOT added to that file, because it has only +21 lines of headroom against the 500-line ceiling and a same-shape companion would consume all of it; the off switch is +pinned instead by GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain in the new file +`UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` that [P1-T8] creates. (2) ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection at `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs`:212-243 asserts at lines 219-226 that an EMPTY archive root strips exactly one leading separator. That is the behaviour AC4 eliminates, so [P1-T14] rewrites the @@ -281,6 +292,7 @@ the derived percentages used instead when the denominators differ. The aggregati whose exact form is `LINES_COVERED= LINES_VALID= BRANCHES_COVERED= BRANCHES_VALID= PACKAGES_MATCHED=`, so the values the acceptance conditions read are values the block definitively prints. `PACKAGES_MATCHED` exists so a package-name mismatch surfaces as a loud zero instead of a silent zero-counter run. +The aggregation deliberately counts every `line` element under a matched package, which selects the class-level and the method-level elements alike and therefore over-counts the denominator relative to a de-duplicated per-line count. That is sound for the comparison it exists to make, because the identical method is applied to both documents, but the resulting percentage is a comparability index and NOT the repository line-coverage rate the 80 percent policy floor is defined over. [P0-T12] therefore records `BASELINE_FLOOR:` explicitly qualified as measured against the comparability index, and no task in this plan gates on it. **D14 — Assembly discovery excludes worktree copies by construction.** The nine first-party test assemblies are named explicitly on every run command. A path that is never enumerated cannot be loaded, so no worktree under a `.claude` segment @@ -306,7 +318,7 @@ can enter a run. | `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` | CREATE | | `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` | CREATE | | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` | CREATE | -| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` | MODIFY — 479 lines, budget +21; retarget (D9) | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` | MODIFY — 479 lines, budget +4 (ceiling 483, see [P1-T13]); retarget (D9) | | `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` | MODIFY — 354 lines; retarget (D9) | | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` | NO HUNK — re-derived finding (D8) | | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` | NO HUNK — re-derived finding (D8) | @@ -345,7 +357,7 @@ the folder wrapper source file; and the ToDoModel email-utilities sort file. | UtilitiesCS.Test/OutlookObjects/Folder/FolderHierarchyProviderAdapterTests.cs | 258 lines; 6 provider constructions at :98, 122, 142, 170, 185, 202 | | UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs | 1066 lines, pre-existing over-ceiling and NOT touched; FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents :165-189 with the relative recent `Recent\One` :175; Issue609 projection tests :191-247; AddRecents_WhenRecentsExist_AppendsHeaderAndEntries :249-265 with relative recents :255; GetOlSubpath assertions :577-591 | | UtilitiesCS.Test/OutlookObjects/Folder/FolderRowTests.cs | text-parity test :29-70 with the relative recent :39; FolderRowArray_DoesNotAlterFolderArrayOutput :73-101; FindFolderRows parity :104; recents mock helper :248-255 | -| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` | 455 lines; namespace QuickFiler.Test.Controllers :12; strict provider mocks :30, 125, 261, 311, 385; lineage test :20-116 asserting the archive-root index :109 and :113 and the 73 percent cell :114; rooted-target test :118-166 binding a rooted score at :149; boundary test :302-377 building an inline Archive segment :327; slash-only-root test :379-427 asserting segment-0 activation yields `\Archive` :426; shared Chain helper :434-448 emitting the leading Archive segment :444; Segment helper :450-453 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs` | 455 lines; namespace QuickFiler.Test.Controllers :12; six strict provider mocks :30, 125, 178, 261, 311, 385; six tests, one per mock; lineage test :20-116 asserting the archive-root index :109 and :113 and the 73 percent cell :114; rooted-target test :118-166 binding a rooted score at :149; boundary test :302-377 building an inline Archive segment :327; slash-only-root test :379-427 asserting segment-0 activation yields `\Archive` :426; shared Chain helper :434-448 emitting the leading Archive segment :444; Segment helper :450-453 | | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs` | 253 lines; strict provider mocks :21, 70, 118, 186; four uses of the shared Chain helper :33, 82, 130, 196 | | `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` | 354 lines; namespace QuickFiler.Controllers.Tests :10; `public partial class QfcItemController_FolderHandlingTests` :21; ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection :212-243 with the empty-root one-separator assertion :219-226 and the case-insensitive assertion :239-242; the FolderContains-boundary empty-root test doc :245-258 | | QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs | 358 lines; namespace QuickFiler.Test.Controllers :16; rooted/relative pair bind :254-259 | @@ -409,7 +421,7 @@ foreach ($proj in @('UtilitiesCS\UtilitiesCS.csproj', 'QuickFiler\QuickFiler.csp } ``` -- [ ] [P0-T5] Restore the manifest-pinned dotnet tools and record `/evidence/baseline/p0-t5-tools.md`. Acceptance: the artifact records `EXIT_CODE: 0` and `dotnet tool run csharpier --version` prints `1.2.6`. +- [ ] [P0-T5] Restore the manifest-pinned dotnet tools and record `/evidence/baseline/p0-t5-tools.md`. Acceptance: the artifact records `EXIT_CODE: 0` and the printed version string contains the substring 1.2.6. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path @@ -418,11 +430,16 @@ dotnet tool restore dotnet tool run csharpier --version ``` -- [ ] [P0-T6] Resolve `dotnet-coverage` and record `/evidence/baseline/p0-t6-dotnet-coverage.md`. Run `dotnet-coverage --version` first; only if that probe exits non-zero, run `dotnet tool install --global dotnet-coverage` and re-probe. Acceptance: the artifact records a final `dotnet-coverage --version` invocation with `EXIT_CODE: 0` and the printed version string, and states which of the two branches was taken. +- [ ] [P0-T6] Resolve `dotnet-coverage` and record `/evidence/baseline/p0-t6-dotnet-coverage.md`. Probe with `Get-Command dotnet-coverage -ErrorAction SilentlyContinue` rather than by running the tool, because an unresolvable command name raises a PowerShell CommandNotFoundException instead of setting a non-zero exit code, so there is no exit code for a branch condition to read. Only when the probe returns nothing, run `dotnet tool install --global dotnet-coverage`, then prepend the user-profile global-tool directory to PATH and re-probe; that prepend is required only on this branch, because a shell that was already running when the tool was installed does not inherit the new directory. On this host the probe branch is the expected one: the completed issue #791 run recorded the tool resolving with no PATH amendment. Acceptance: the artifact records a final `dotnet-coverage --version` invocation with `EXIT_CODE: 0` and the printed version string, states which of the two branches was taken, and records the derived line `DOTNET-COVERAGE-ON-PATH: true`. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path $env:PATH = "$env:DOTNET_ROOT;$env:PATH" +$probe = Get-Command dotnet-coverage -ErrorAction SilentlyContinue +if ($null -eq $probe) { + dotnet tool install --global dotnet-coverage + $env:PATH = "$env:USERPROFILE\.dotnet\tools;$env:PATH" +} dotnet-coverage --version ``` @@ -455,7 +472,7 @@ msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true ``` -- [ ] [P0-T11] Run the UtilitiesCS.Test and QuickFiler.Test assemblies and record their pass/fail counts into `/evidence/baseline/p0-t11-suites.md` as the derived lines `BASELINE-UT-TOTAL:`, `BASELINE-UT-PASSED:`, `BASELINE-UT-FAILED:`, `BASELINE-QFT-TOTAL:`, `BASELINE-QFT-PASSED:`, `BASELINE-QFT-FAILED:`, read from the TRX `ResultSummary/Counters` element of each run. Do not paste TRX content (R3). Acceptance: all six derived lines are present and numeric, each `BASELINE-*-FAILED` recorded whatever its value, and the artifact names the four excluded shell-icon classes (R13). +- [ ] [P0-T11] Run the UtilitiesCS.Test and QuickFiler.Test assemblies and record their pass/fail counts into `/evidence/baseline/p0-t11-suites.md` as the derived lines `BASELINE-UT-TOTAL:`, `BASELINE-UT-PASSED:`, `BASELINE-UT-FAILED:`, `BASELINE-QFT-TOTAL:`, `BASELINE-QFT-PASSED:`, `BASELINE-QFT-FAILED:`, read from the TRX `ResultSummary/Counters` element of each run. Do not paste TRX content (R3). Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: all six derived lines are present and numeric, each `BASELINE-*-FAILED` recorded whatever its value, and the artifact names the four excluded shell-icon classes (R13); the artifact additionally records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema, and each `BASELINE-*-FAILED` value is read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -464,7 +481,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p0-t11-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook' ``` -- [ ] [P0-T12] Run the full nine-assembly suite under `dotnet-coverage` and record `/evidence/baseline/p0-t12-coverage.md` with the derived lines `BASELINE-LINES-COVERED:`, `BASELINE-LINES-VALID:`, `BASELINE-BRANCHES-COVERED:`, `BASELINE-BRANCHES-VALID:`, `BASELINE-PACKAGES-MATCHED:`, the two derived percentages, and `BASELINE-TOTAL-TESTS:` / `BASELINE-FAILED-TESTS:`. The four counters come from the pinned aggregation block below, whose success-case output is the single line it prints itself, of the form `LINES_COVERED= LINES_VALID= BRANCHES_COVERED= BRANCHES_VALID= PACKAGES_MATCHED=` (D13). Record `BASELINE_FLOOR: MET` or `BASELINE_FLOOR: NOT MET` against the 80 percent line floor and continue either way; a pre-existing repository floor never halts this plan. Acceptance: the five `BASELINE-` counter lines are present and numeric and `BASELINE-PACKAGES-MATCHED` is greater than zero. +- [ ] [P0-T12] Run the full nine-assembly suite under `dotnet-coverage` and record `/evidence/baseline/p0-t12-coverage.md` with the derived lines `BASELINE-LINES-COVERED:`, `BASELINE-LINES-VALID:`, `BASELINE-BRANCHES-COVERED:`, `BASELINE-BRANCHES-VALID:`, `BASELINE-PACKAGES-MATCHED:`, the two derived percentages, and `BASELINE-TOTAL-TESTS:` / `BASELINE-FAILED-TESTS:`. The four counters come from the pinned aggregation block below, whose success-case output is the single line it prints itself, of the form `LINES_COVERED= LINES_VALID= BRANCHES_COVERED= BRANCHES_VALID= PACKAGES_MATCHED=` (D13). Record `BASELINE_FLOOR: MET` or `BASELINE_FLOOR: NOT MET` against the 80 percent line floor, explicitly qualified as measured against the D13 comparability index rather than against the repository line-coverage rate, and continue either way; a pre-existing repository floor never halts this plan. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: the five `BASELINE-` counter lines are present and numeric and `BASELINE-PACKAGES-MATCHED` is greater than zero. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path @@ -497,7 +514,7 @@ foreach ($pkg in $doc.SelectNodes('//package')) { "LINES_COVERED=$lc LINES_VALID=$lv BRANCHES_COVERED=$bc BRANCHES_VALID=$bv PACKAGES_MATCHED=$pm" ``` -- [ ] [P0-T13] Determine, from coverage\799-baseline.cobertura.xml, which Write Set production files are measurable, and write `/evidence/baseline/p0-t13-measurability.md`. For each of the seven EXISTING Write Set production paths, query for a `class` element whose `filename` attribute ends with a directory separator followed by that file's name, and record one line per file of the form `MEASURABLE: ` or `UNMEASURABLE: `. The trailing-name match must be separator-anchored, because an unanchored suffix over-selects a sibling whose name merely ends with the same characters. The two files this plan CREATES are recorded separately as `NEW: ` and are measured for the first time by [P3-T9]. Acceptance: exactly seven `MEASURABLE:`/`UNMEASURABLE:` lines are present, one per existing Write Set production path, and the artifact records the class-element counts the determination was made from. +- [ ] [P0-T13] Determine, from coverage\799-baseline.cobertura.xml, which Write Set production files are measurable, and write `/evidence/baseline/p0-t13-measurability.md`. For each of the six EXISTING Write Set production paths, and additionally for UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs, query for a `class` element whose `filename` attribute ends with a directory separator followed by that file's name, and record one line per file of the form `MEASURABLE: ` or `UNMEASURABLE: `. The trailing-name match must be separator-anchored, because an unanchored suffix over-selects a sibling whose name merely ends with the same characters. The two files this plan CREATES are recorded separately as `NEW: ` and are measured for the first time by [P3-T9]. Acceptance: exactly seven `MEASURABLE:`/`UNMEASURABLE:` lines are present — one for each of the six EXISTING Write Set production paths, plus one for UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs, which this plan does not modify but whose measurability is recorded because [P2-T1], [P2-T2] and [P2-T9] all route through it and a zero-class-element result there would explain an otherwise puzzling [P3-T7] outcome — and the artifact records the class-element counts the determination was made from. ```powershell $doc = New-Object System.Xml.XmlDocument @@ -513,9 +530,9 @@ foreach ($n in $names) { } ``` -- [ ] [P0-T14] Record the baseline line count of every file this plan edits or creates into `/evidence/baseline/p0-t14-sizes.md`, one ` = ` line per file, covering the seven existing Write Set production paths, the four existing Write Set test paths, and the two #439 test paths D8 marks NO HUNK, plus a `CEILING: 500 (applies to *.cs only)` line. Record the four project files separately under a `PROJECT-FILE (exempt): = ` heading with the R8 reason. Record the three pre-existing over-ceiling files under a `PRE-EXISTING OVER CEILING:` heading with the D11 budgets. Acceptance: every `.cs` path has a numeric count; the four project-file counts are recorded under the exempt heading with their reason; the artifact states that `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is exactly 500 and that `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` has 21 lines of headroom. +- [ ] [P0-T14] Record the baseline line count of every file this plan edits or creates into `/evidence/baseline/p0-t14-sizes.md`, one ` = ` line per file, covering the six existing Write Set production paths, the two retargeted Write Set test paths, and the two #439 test paths D8 marks NO HUNK, plus a `CEILING: 500 (applies to *.cs only)` line. Record the four project files separately under a `PROJECT-FILE (exempt): = ` heading with the R8 reason. Record the three pre-existing over-ceiling files under a `PRE-EXISTING OVER CEILING:` heading with the D11 budgets. Acceptance: every `.cs` path has a numeric count; the four project-file counts are recorded under the exempt heading with their reason; the artifact states that `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is exactly 500 and that `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` has 21 lines of headroom. -- [ ] [P0-T15] Record the pre-change status of the tests this plan retargets or must keep green into `/evidence/baseline/p0-t15-tests.md`, one line per test of the form `BASELINE-PASS: ` or `BASELINE-FAIL: `, derived from the two TRX documents [P0-T11] wrote. The set is: the two retarget targets named in D9 (GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments and ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection); the recents and projection tests named in D9 that must stay green (FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents, AddRecents_WhenRecentsExist_AppendsHeaderAndEntries, Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths, Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath, FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds, GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment); and the five #439 router tests D8 marks NO HUNK (Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability, Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively, Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows, Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic, Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection). This is the set that makes the Phase 2 no-newly-failing comparison meaningful. Acceptance: thirteen `BASELINE-PASS:` or `BASELINE-FAIL:` lines are present, one per named test, each derived from a TRX this plan wrote. +- [ ] [P0-T15] Record the pre-change status of the tests this plan retargets or must keep green into `/evidence/baseline/p0-t15-tests.md`, one line per test of the form `BASELINE-PASS: ` or `BASELINE-FAIL: `, derived from the two TRX documents [P0-T11] wrote. The set is: the two retarget targets named in D9 (GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments and ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection); the recents and projection tests named in D9 that must stay green (FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents, AddRecents_WhenRecentsExist_AppendsHeaderAndEntries, Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths, Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath, FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds, GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment); and the ten tests of the partial class `BreadcrumbBridgeRouterIssue439Tests`, across both files, which D8 marks NO HUNK (Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability, Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively, Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome, Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows, Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic, Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection, Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget, Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget, Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget and Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild). The third of those is the one that pins today's null-chain selectable-fallback rendering, which is exactly the path [P2-T12] modifies, so omitting it would leave the modified path unguarded. This is the set that makes the Phase 2 no-newly-failing comparison meaningful. Acceptance: eighteen `BASELINE-PASS:` or `BASELINE-FAIL:` lines are present, one per named test, each derived from a TRX this plan wrote. --- @@ -526,13 +543,13 @@ new tests name types and members that do not exist yet and a missing declaration time rather than producing a targeted failure. No production behaviour changes in Phase 1: every seam body either throws `NotImplementedException` or stores a value nothing reads yet. -- [ ] [P1-T1] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveStemProjection` and the single member `public static string ToDisplayStem(string folderPath, string archiveRoot)` whose body is `throw new NotImplementedException("Issue #799: the display projection body is supplied by [P2-T1].");`. The XML doc must state that this is a LENIENT DISPLAY projection returning the archive-relative stem when the path is strictly under the root and the input unchanged in every other case including an empty or whitespace root, and must state why it is a separate type rather than a member on the strict contract: that contract is a hard boundary that yields an empty string on failure and never passes its input through, and every display site needs the opposite fallback, so a lenient overload there would blur the invariant #614 created it for. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `ToDisplayStem`. +- [ ] [P1-T1] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveStemProjection` and the single member `public static string? ToDisplayStem(string? folderPath, string? archiveRoot)` whose body is `throw new NotImplementedException("Issue #799: the display projection body is supplied by [P2-T1].");`. Both parameters and the return carry the `?` annotation because this file opens with `#nullable enable`, because [P1-T6] pins a null-path case, and because two of its three production callers — `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` and `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` — are themselves `#nullable enable` files whose argument expressions are nullable, so an unannotated parameter is CS8604 at those call sites under the [P3-T4] gate. The XML doc must state that this is a LENIENT DISPLAY projection returning the archive-relative stem when the path is strictly under the root and the input unchanged in every other case including an empty or whitespace root, and must state why it is a separate type rather than a member on the strict contract: that contract is a hard boundary that yields an empty string on failure and never passes its input through, and every display site needs the opposite fallback, so a lenient overload there would blur the invariant #614 created it for. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `ToDisplayStem`. -- [ ] [P1-T2] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveChainProjection` and the single member `public static bool TryTrimBelowArchiveRoot(IReadOnlyList chain, string archiveRoot, out IReadOnlyList trimmed)` whose body assigns an empty array to `trimmed` and then is `throw new NotImplementedException("Issue #799: the chain trim body is supplied by [P2-T2].");`. The XML doc must state that the archive-root node is the first chain index whose segment FolderPath is the root itself, detected as TryMakeArchiveRelative returning true with an empty stem, that the method returns the remainder of the chain after that index, and that it returns false when no such index exists and also when that index is the last one, because the leaf is then the root and there is nothing to render below it. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `TryTrimBelowArchiveRoot`. +- [ ] [P1-T2] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveChainProjection` and the single member `public static bool TryTrimBelowArchiveRoot(IReadOnlyList? chain, string? archiveRoot, out IReadOnlyList trimmed)` whose body assigns an empty array to `trimmed` and then is `throw new NotImplementedException("Issue #799: the chain trim body is supplied by [P2-T2].");`. The `chain` and `archiveRoot` parameters carry the `?` annotation because [P2-T2] specifies a false return for a null chain and for a null or whitespace root, and this file opens with `#nullable enable`; `trimmed` stays unannotated because it is assigned an empty array on every path before return. The XML doc must state that the archive-root node is the first chain index whose segment FolderPath is the root itself, detected as TryMakeArchiveRelative returning true with an empty stem, that the method returns the remainder of the chain after that index, and that it returns false when no such index exists and also when that index is the last one, because the leaf is then the root and there is nothing to render below it. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `TryTrimBelowArchiveRoot`. - [ ] [P1-T3] Add two one-line self-closing Compile Include entries to `UtilitiesCS/UtilitiesCS.csproj` for the two new production helpers, adjacent to the existing ArchiveStemContract.cs entry at line 623. The project is legacy non-SDK with an insertion-ordered item list and no globbing, so the entries are mandatory and their placement follows the neighbouring folder entries. Acceptance: the project file contains exactly two new Compile Include lines and the two named files compile. -- [ ] [P1-T4] In `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`, add the declaration-only seams that Phase 1 tests bind against, changing no existing behaviour. Add `public interface IFolderLabelAbsenceReport` with the single member `bool IsAbsentLabel(string folderPath);` and an XML doc recording D4's reason for a separate interface rather than a fourth member on IFolderHierarchyProvider. Declare that the provider implements it, with `public bool IsAbsentLabel(string folderPath) => throw new NotImplementedException("Issue #799: the absence report body is supplied by [P2-T6].");`. Add the optional second constructor parameter `System.Func archiveRootAccessor = null`, stored in the internal get-only auto-property `ArchiveRootAccessor` (D3), leaving the existing null check on the first parameter exactly as it is. Add `internal System.Action ErrorSink { get; set; }` with an XML doc naming it the injected diagnostic sink that tests observe instead of attaching a log4net appender, so no test mutates the process-global logger repository. Do not change GetAncestorChainAsync, GetImmediateSubfoldersAsync, ResolveLeafKeyAsync or ResolveByUniqueSuffix in this task. Acceptance: the solution compiles; the constructor declares exactly two parameters, the second optional with a null default; and a case-sensitive search of the file finds the single-line tokens `IFolderLabelAbsenceReport`, `ArchiveRootAccessor` and `ErrorSink`. +- [ ] [P1-T4] In `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`, add the declaration-only seams that Phase 1 tests bind against, changing no existing behaviour. Add `public interface IFolderLabelAbsenceReport` with the single member `bool IsAbsentLabel(string folderPath);` and an XML doc recording D4's reason for a separate interface rather than a fourth member on IFolderHierarchyProvider. Declare that the provider implements it, with `public bool IsAbsentLabel(string folderPath) => throw new NotImplementedException("Issue #799: the absence report body is supplied by [P2-T6].");`. Add the optional second constructor parameter `System.Func? archiveRootAccessor = null`, stored in the internal get-only auto-property `internal System.Func? ArchiveRootAccessor { get; }` (D3), leaving the existing null check on the first parameter exactly as it is. Add `internal System.Action? ErrorSink { get; set; }` with an XML doc naming it the injected diagnostic sink that tests observe instead of attaching a log4net appender, so no test mutates the process-global logger repository. Every one of these three declarations carries the `?` annotation because this file opens with `#nullable enable` at line 1 and the [P3-T4] gate runs `/p:TreatWarningsAsErrors=true`: an unannotated `System.Func … = null` is CS8625 and an unannotated never-initialised `System.Action` property is CS8618, and both are promoted to build errors by that gate. Do not change GetAncestorChainAsync, GetImmediateSubfoldersAsync, ResolveLeafKeyAsync or ResolveByUniqueSuffix in this task. Acceptance: the solution compiles; the constructor declares exactly two parameters, the second optional with a null default; and a case-sensitive search of the file finds the single-line tokens `IFolderLabelAbsenceReport`, `ArchiveRootAccessor` and `ErrorSink`; and the analyzer build and the nullable build both exit 0 with the declarations in place, which is the observation that proves no CS8625 or CS8618 was introduced. - [ ] [P1-T5] Build the solution so the seam declarations are available to the test projects, and record `/evidence/regression-testing/p1-t5-seam-build.md`. Acceptance: `EXIT_CODE: 0`, which also proves D2's claim that the optional parameter leaves all 19 existing provider constructions compiling unchanged, including the single-null-argument construction at UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs:316. @@ -550,11 +567,11 @@ msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" - [ ] [P1-T10] Add four one-line self-closing Compile Include entries to `UtilitiesCS.Test/UtilitiesCS.Test.csproj` for the four new test files, appended adjacent to the existing folder-test entries at lines 302-304. The project is legacy non-SDK with an insertion-ordered item list and no globbing. Acceptance: the project file contains exactly four new Compile Include lines and the four named files compile. -- [ ] [P1-T11] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` with `[TestClass] public sealed class BreadcrumbBridgeRouterScoreJoinTests` in namespace `QuickFiler.Test.Controllers`, modelled on the strict-mock construction already used at QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs:254-259 (a `Mock`, a `Mock` capturing NavigateToString output, a real `BreadcrumbMessageCodec`, a real `BreadcrumbHtmlRenderer` and a real `BreadcrumbOutboundQueue`). It contains: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` (the AC6 pin: presented row is the archive-relative stem, the score carries the raw rooted path, the bound root is non-empty; assert the rendered document contains the percentage cell for that row); `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage` (D7's additive requirement: the rooted-presented case must not regress); `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` (the public three-argument overload forwards an empty root, so the projection is the identity and no existing caller changes behaviour); `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` (the AC3 pin, asserted as its own test and not as an incidental consequence: with an ancestor chain that begins below the archive root, the bound row's filing target and the joined score key are both still the archive-relative stem); and `BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly` (the spec's integration scenario, driven entirely through the router with no WebView2 and no Outlook: a banner row, a suggestion row, a search-result row, the trash pseudo-row and one stale label; assert lineage on both folder row kinds, the existing fallback on the stale label, and no lineage on the banner or trash rows). Every provider interaction is Moq-supplied, so the AC7 suppression path is inert in this file by construction (D4). Acceptance: the file compiles once [P1-T12] wires it, contains exactly five `[TestMethod]` attributes, and is at or below 500 lines. +- [ ] [P1-T11] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` with `[TestClass] public sealed class BreadcrumbBridgeRouterScoreJoinTests` in namespace `QuickFiler.Test.Controllers`, modelled on the strict-mock construction already used at QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs:254-259 (a `Mock`, a `Mock` capturing NavigateToString output, a real `BreadcrumbMessageCodec`, a real `BreadcrumbHtmlRenderer` and a real `BreadcrumbOutboundQueue`). It contains: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` (the AC6 pin: presented row is the archive-relative stem, the score carries the raw rooted path, the bound root is non-empty; assert the rendered document contains the percentage cell for that row); `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage` (D7's additive requirement: the rooted-presented case must not regress); `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` (the public three-argument overload forwards an empty root, so the projection is the identity and no existing caller changes behaviour); `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` (the AC3 pin, asserted as its own test and not as an incidental consequence: with an ancestor chain that begins below the archive root, the bound row's filing target and the joined score key are both still the archive-relative stem); and `BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly` (the spec's integration scenario, driven entirely through the router with no WebView2 and no Outlook: a banner row, a suggestion row, a search-result row, the trash pseudo-row and one stale label; assert lineage on both folder row kinds, the existing fallback on the stale label, and no lineage on the banner or trash rows); `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned` (the AC7 row half, and the only test in this plan that executes the [P2-T12] branch: the provider mock is extended with `.As()` so the router's `provider as IFolderLabelAbsenceReport` cast succeeds, `IsAbsentLabel` is set up to return true for exactly one suggestion row's hierarchy path and false for the others, and `FetchChainAsync` is driven to a null chain for that row; assert the rendered document contains no row for the suppressed label, that every surviving row still carries its own segment keys, and that the surviving row count is one lower than the presented row count); and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed` (decision D-B's zero-candidate restriction at the router boundary: same construction, but `IsAbsentLabel` returns false for the null-chain row, and the row must still be rendered with the existing fallback). Extending the mock with `.As()` is confined to this file and reaches no existing test: no other router test in QuickFiler.Test uses `.As<>()`, so in every one of them the cast still yields null and suppression stays inert exactly as D4 records. Author this file for C# 7.3. QuickFiler.Test.csproj declares no `` and targets v4.8.1 at its line 18, so it compiles at the 7.3 default while every other project in this plan's scope is at Latest, preview or 12.0. Use classic `using (...) { }` blocks, `!= null` rather than `is not null`, explicitly typed `new` rather than target-typed `new`, `switch` statements rather than switch expressions, and no nullable reference annotation anywhere in the file. Mirroring a construct from an existing UtilitiesCS.Test file into this one is the specific failure mode: it surfaces as CS8370 at the [P1-T15] build, not at edit time. Acceptance: the file compiles once [P1-T12] wires it, contains exactly seven `[TestMethod]` attributes, and is at or below 500 lines. - [ ] [P1-T12] Add one one-line self-closing Compile Include entry to `QuickFiler.Test/QuickFiler.Test.csproj` for the new test file, appended adjacent to the existing BreadcrumbBridgeRouterIssue439Tests.cs entry at line 64. Acceptance: the project file contains exactly one new Compile Include line and the named file compiles. -- [ ] [P1-T13] Retarget GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments in `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` lines 72-91 so the pinned configuration is the one production uses (D9): rename it to `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`, construct the provider with a root accessor returning the fixture's archive-root path, and replace the root-to-leaf assertion at lines 84-87 with the trimmed expectation. Immediately after it, add the companion case `GetAncestorChainAsync_WithoutRootAccessor_ReturnsRootToLeafSegments` retaining the original untrimmed expectation for the no-accessor construction, so the off switch stays pinned. Retargeting rather than deleting is required because the deleted behaviour is a specification change and a deleted test pins nothing. The file is 479 lines and the ceiling is 500, so the net addition must not exceed 21 lines; the original body is reused for the companion case rather than duplicated wholesale. Acceptance: a case-sensitive search of the file finds zero matches for the single-line token `GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments` and one match each for `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath` and `GetAncestorChainAsync_WithoutRootAccessor_ReturnsRootToLeafSegments`; the twelve other provider constructions in the file are unchanged; and the file is at or below 500 lines. +- [ ] [P1-T13] Retarget GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments in `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` lines 72-91 so the pinned configuration is the one production uses (D9): rename it to `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`, construct the provider with a root accessor returning the fixture's `\Root` path, and replace the root-to-leaf assertion at lines 84-87 with the trimmed expectation `\Root\Clients`, `\Root\Clients\Acme`. Retargeting rather than deleting is required because the deleted behaviour is a specification change and a deleted test pins nothing. No companion case is added HERE. The off switch is pinned instead by `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain` in the new file `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` that [P1-T8] creates, which has ample headroom. This placement is a hard constraint rather than a preference: the file edited here is 479 lines against a 500-line ceiling, its existing test body is 20 lines, and a same-shape companion plus its blank separator would consume 21 of the 21 available lines before the one line the added constructor argument itself costs and before CSharpier reflows anything, which would put the file over the ceiling and make the [P3-T10] gate unsatisfiable with no remedy. Acceptance: a case-sensitive search of the file finds zero matches for the single-line token `GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments` and exactly one match for `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`; the twelve other provider constructions in the file are unchanged; and the file is at or below 483 lines, that is its [P0-T14] baseline of 479 plus at most four lines. - [ ] [P1-T14] Retarget the empty-root assertion in `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` lines 219-226, inside ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection, so it asserts the identity projection rather than the one-separator strip AC4 eliminates: an empty archive root must now leave the input unchanged, and the assertion's `because` text must state that AC4 of issue #799 removed the empty-root strip. Update the surrounding XML doc at lines 206-211 so it no longer describes the removed behaviour, and update the XML doc at lines 245-258 of the following test, whose prose asserts that an empty archive root causes FolderArray entries to be stripped; that following test's own assertions are re-derived against the new behaviour and updated only where they encode the removed strip. The five other boundary assertions in the retargeted test (null root, null path, out-of-root path, empty-remainder guard, case-insensitive prefix) are unchanged, because ToDisplayStem reproduces each of them. Acceptance: a case-sensitive search of the file finds zero matches for the single-line literal `archivePrefix of one separator, which it strips`; the retargeted test still contains exactly six assertions on ProjectPredeterminedFolder; and the file is at or below 500 lines. @@ -564,7 +581,7 @@ msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" ``` -- [ ] [P1-T16] [expect-fail] Run the four new and one retargeted UtilitiesCS.Test classes and record `/evidence/regression-testing/p1-t16-ut-fail.md` with `ExpectedExitCode: 1`. The artifact must enumerate, by fully qualified name, every failing test and state for each whether it fails because a Phase 1 seam throws `NotImplementedException` (tag `SEAM-BLOCKED`) or because the production behaviour is not yet changed (tag `NEW` or `RETARGETED`). Acceptance: `EXIT_CODE: 1`, and the recorded failure set includes `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` and `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`. +- [ ] [P1-T16] [expect-fail] Run the four new and one retargeted UtilitiesCS.Test classes and record `/evidence/regression-testing/p1-t16-ut-fail.md` with `ExpectedExitCode: 1`. The artifact must enumerate, by fully qualified name, every failing test and state for each whether it fails because a Phase 1 seam throws `NotImplementedException` (tag `SEAM-BLOCKED`) or because the production behaviour is not yet changed (tag `NEW` or `RETARGETED`). Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: `EXIT_CODE: 1`, and the recorded failure set includes `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` and `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -572,7 +589,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf & $vstest UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p1-t16' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~ArchiveStemProjectionTests|FullyQualifiedName~ArchiveChainProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTrimTests|FullyQualifiedName~FolderPredictorRecentsProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTests' ``` -- [ ] [P1-T17] [expect-fail] Run the new QuickFiler.Test score-join class and the retargeted folder-handling class and record `/evidence/regression-testing/p1-t17-qft-fail.md` with `ExpectedExitCode: 1`, enumerating each failing test by fully qualified name with its failure message reduced per R3. Acceptance: `EXIT_CODE: 1`, and both `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` and `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` appear in the failure set. +- [ ] [P1-T17] [expect-fail] Run the new QuickFiler.Test score-join class and the retargeted folder-handling class and record `/evidence/regression-testing/p1-t17-qft-fail.md` with `ExpectedExitCode: 1`, enumerating each failing test by fully qualified name with its failure message reduced per R3. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: `EXIT_CODE: 1`, and both `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` and `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` appear in the failure set. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -586,19 +603,19 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf ### Phase 2 — Production implementation -- [ ] [P2-T1] Replace the [P1-T1] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` with the real implementation: return the stem when `ArchiveStemContract.TryMakeArchiveRelative(folderPath, archiveRoot, out var stem)` returns true AND `stem.Length > 0`, and return `folderPath` unchanged in every other case. The length condition is what makes "path equals root" non-projectable, because the contract returns true with an empty stem on exact equality at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:124-127 and an empty display row is worse than the full path; it reproduces the existing guard at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`:858 exactly. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the display projection body is supplied by`. +- [ ] [P2-T1] Replace the [P1-T1] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` with the real implementation: guard first with `if (folderPath is null || archiveRoot is null) { return folderPath; }`, then return the stem when `ArchiveStemContract.TryMakeArchiveRelative(folderPath, archiveRoot, out var stem)` returns true AND `stem.Length > 0`, and return `folderPath` unchanged in every other case. The leading null guard is mandatory rather than defensive: `ArchiveStemContract.TryMakeArchiveRelative` declares its two input parameters as non-nullable `string` at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:107-108 inside a file that opens with `#nullable enable` at line 1, so passing either [P1-T1] `string?` parameter into it without first narrowing is CS8604, which the [P3-T4] gate promotes to a build error. The guard also delivers the behaviour [P1-T6] pins for the null-path case, so it costs no extra branch. The length condition is what makes "path equals root" non-projectable, because the contract returns true with an empty stem on exact equality at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:124-127 and an empty display row is worse than the full path; it reproduces the existing guard at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`:858 exactly. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the display projection body is supplied by`. -- [ ] [P2-T2] Replace the [P1-T2] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` with the real implementation: scan the chain for the first index whose segment FolderPath satisfies `ArchiveStemContract.TryMakeArchiveRelative(segment.FolderPath, archiveRoot, out var stem)` returning true with `stem.Length == 0`, which is exactly the equality case; assign the remainder after that index to `trimmed` and return true; return false with an empty `trimmed` when no such index exists, when the chain is null or empty, and when that index is the last element. A null or whitespace `archiveRoot` returns false, which follows from the contract's own guard at ArchiveStemContract.cs:113 and needs no separate branch. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the chain trim body is supplied by`. +- [ ] [P2-T2] Replace the [P1-T2] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` with the real implementation: scan the chain for the first index whose segment FolderPath satisfies `ArchiveStemContract.TryMakeArchiveRelative(segment.FolderPath, archiveRoot, out var stem)` returning true with `stem.Length == 0`, which is exactly the equality case; assign the remainder after that index to `trimmed` and return true; return false with an empty `trimmed` when no such index exists, when the chain is null or empty, and when that index is the last element. A null `chain` or a null `archiveRoot` returns false through an explicit leading guard, and a whitespace-only `archiveRoot` returns false through the contract's own guard at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:113. The null half of that guard is mandatory rather than defensive: the contract declares its two input parameters as non-nullable `string` at ArchiveStemContract.cs:107-108 inside a file that opens with `#nullable enable` at line 1, so passing the [P1-T2] `string? archiveRoot` into it without first narrowing is CS8604, which the [P3-T4] gate promotes to a build error. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the chain trim body is supplied by`. - [ ] [P2-T3] Relocate the breadcrumb pipeline helper OUT of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` and INTO the new file `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task runs BEFORE [P2-T14] adds the constructor argument; the reverse order puts the viewer-setup file at 501 lines in an intermediate state (R9). Move lines 132-163 verbatim — the two-part comment at 132-136, the `[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]` attribute at 137, and the member body at 138-163 — together with the blank line that separated it from its neighbour. The new file declares `internal partial class QfcItemController` in namespace `QuickFiler.Controllers` and carries only the using directives the moved code needs, which are the ones for the concrete viewer type and for the arrow-event handler; the provider is referenced by its fully qualified name in the moved code and needs no using. This is a pure relocation: no statement is added, removed or reordered. Acceptance: a case-sensitive search of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` returns zero matches for the single-line token `EnsureBreadcrumbPipeline` and that file's line count is at least 30 lines lower than the [P0-T14] baseline of 500; a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` returns one match for that token and one for the single-line token `ExcludeFromCodeCoverage`. - [ ] [P2-T4] Add one one-line self-closing Compile Include entry to `QuickFiler/QuickFiler.csproj` for the new item-controller partial, adjacent to the existing QfcItemController.ViewerSetup.cs entry at line 335. Acceptance: the project file contains exactly one new Compile Include line and the solution compiles, which is the only proof that the relocated member is still in the build. -- [ ] [P2-T5] Apply the AC1 and AC2 trim inside GetAncestorChainAsync at `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` lines 34-42, after the snapshot walk and before segment mapping, so row order, banner placement and the trash pseudo-row are untouched. Read the root lazily through the [P1-T4] `ArchiveRootAccessor` property inside a try that treats any exception from the accessor as "no trim configured", which is what keeps the existing archive-root-throws behaviour intact (D2). When the accessor is null or yields a null, empty or whitespace root, return the mapped chain unchanged, which is the effective off switch. Otherwise call `ArchiveChainProjection.TryTrimBelowArchiveRoot`; on true, map and return the trimmed segments; on false, emit one error through the log4net ILog already declared at lines 17-19 AND through the [P1-T4] `ErrorSink`, and return `Array.Empty()`, which routes the Efc surface into the empty-chain single-segment fallback at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:123-131 and the QuickFiler surface into its existing scored fallback. Do not modify GetImmediateSubfoldersAsync: subfolders are below the leaf and therefore below the archive root by construction. Acceptance: `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot`, `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain`, `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty`, `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty` and `GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain` all pass, and the two retargeted provider tests from [P1-T13] both pass. +- [ ] [P2-T5] Apply the AC1 and AC2 trim inside GetAncestorChainAsync at `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` lines 34-42, after the snapshot walk and before segment mapping, so row order, banner placement and the trash pseudo-row are untouched. Read the root lazily through the [P1-T4] `ArchiveRootAccessor` property inside a try that treats any exception from the accessor as "no trim configured", which is what keeps the existing archive-root-throws behaviour intact (D2). When the accessor is null or yields a null, empty or whitespace root, return the mapped chain unchanged, which is the effective off switch. Otherwise call `ArchiveChainProjection.TryTrimBelowArchiveRoot`; on true, map and return the trimmed segments; on false, emit one error through the log4net ILog already declared at lines 17-19 AND through the [P1-T4] `ErrorSink`, and return `Array.Empty()`, which routes the Efc surface into the empty-chain single-segment fallback at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:123-131 and the QuickFiler surface into its existing scored fallback. Do not modify GetImmediateSubfoldersAsync: subfolders are below the leaf and therefore below the archive root by construction. Acceptance: `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot`, `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain`, `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty`, `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty` and `GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain` all pass, and the single retargeted provider test from [P1-T13], `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`, also passes. - [ ] [P2-T6] Implement the AC7 log gate and absence classification in `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`. Change ResolveByUniqueSuffix at lines 90-114 from `private static` to a private instance member so it can reach per-instance state, and add the two `ConcurrentDictionary` fields D6 requires, both with `StringComparer.OrdinalIgnoreCase`: a reported-labels set that gates the existing `logger.Error` emission at lines 108-112 through `TryAdd`, so a label already reported by this provider instance emits nothing further, and an absent-labels set that gains the requested path when the candidate count is zero. Keep the message's two causes distinguishable exactly as they are today. Route every emission through the `ErrorSink` as well as through log4net. In ResolveLeafKeyAsync, remove the requested path from the absent-labels set on BOTH success routes — the exact-path match that returns at lines 74-77 and a successful unique-suffix match — so a label that becomes resolvable after a snapshot refresh is no longer reported absent. Replace the [P1-T4] `IsAbsentLabel` seam with a lookup against the absent-labels set. A bare `HashSet` is prohibited here because ResolveLeafKeyAsync awaits AcquireSnapshotAsync and its continuations are not guaranteed to run on one thread; a static set is prohibited because it is process-wide mutable state shared across viewers and across test methods in one assembly. Acceptance: `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` all pass; `ResolveLeafKeyAsync_ArchiveRelativeStem_ResolvesToUniqueSuffixMatchNode` in UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs still passes; and a case-sensitive search of the provider file returns zero matches for the single-line literal `Issue #799: the absence report body is supplied by`. -- [ ] [P2-T7] Replace the ProjectSuggestionPath body at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 848-861 with a delegation to `ArchiveStemProjection.ToDisplayStem`, passing the archive root read defensively from the globals so the existing `_globals is null` identity guard is preserved without a separate early return. This eliminates the empty-root one-separator strip that AC4 names, because the contract returns false for a whitespace-only root at ArchiveStemContract.cs:113. Both call sites are display paths and are unchanged: AddSuggestions at line 810 and AddSuggestionRows at line 842. Acceptance: `Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths` and `Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs both still pass, and `AddSuggestions_WhenSuggestionsExist_AppendsHeaderAndTopSuggestions` still passes. +- [ ] [P2-T7] Replace the ProjectSuggestionPath body at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 848-861 with a delegation to `ArchiveStemProjection.ToDisplayStem`, passing the archive root read defensively from the globals so the existing `_globals is null` identity guard is preserved without a separate early return. This eliminates the empty-root one-separator strip that AC4 names, because the contract returns false for a whitespace-only root at ArchiveStemContract.cs:113. Write the delegation as `ArchiveStemProjection.ToDisplayStem(folderPath, root)!`, with the null-forgiving operator and a one-line comment giving its reason: this file opens with `#nullable enable` at line 1 and ProjectSuggestionPath declares a non-nullable `string` return and a non-nullable `string folderPath` parameter at UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:848, while [P1-T1] declares `ToDisplayStem` with a `string?` return; `ToDisplayStem` returns null only when its own `folderPath` argument is null, which this call site's non-nullable parameter excludes, so the operator is sound and it is required because the unsuppressed form is CS8603 and the [P3-T4] gate promotes it to a build error. Do not change ProjectSuggestionPath's signature: it is called from AddSuggestions at line 810 and AddSuggestionRows at line 842 and widening its return would propagate CS8600 into both. Both call sites are display paths and are unchanged: AddSuggestions at line 810 and AddSuggestionRows at line 842. Acceptance: `Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths` and `Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs both still pass, and `AddSuggestions_WhenSuggestionsExist_AppendsHeaderAndTopSuggestions` still passes. - [ ] [P2-T8] Project each recent entry through the same helper at both AC5 sites in `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`: the string append at line 793 inside AddRecents, and the row-model mirror at line 879 inside AddRecentRows. Both must be projected, because the XML doc at lines 233-242 asserts that the string list and the row list are text-identical and the row list is the one the breadcrumb surfaces actually consume, so projecting only one would break a documented contract. Acceptance: all four `[TestMethod]` tests in `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` pass, and `AddRecents_WhenRecentsExist_AppendsHeaderAndEntries`, `FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents` and `FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds` all still pass, because their fixtures use already-relative recents for which the projection is the identity. @@ -606,9 +623,9 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf - [ ] [P2-T10] Replace the ProjectPredeterminedFolder body at `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` lines 272-285 with a one-line delegation to `ArchiveStemProjection.ToDisplayStem`, retaining the member itself so its existing test keeps a target, and rewrite the XML doc at lines 252-271 so the two paragraphs describing the empty-root divergence are replaced by a statement that both members now share one projection and that the empty-root strip was removed by AC4. Also rewrite the duplication-rationale comment at lines 223-230, whose stated reason — that FolderPredictor.ProjectSuggestionPath is private and lives under UtilitiesCS, which the earlier change could not modify — becomes false the moment the shared helper is public. Acceptance: `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` passes as retargeted by [P1-T14]; a case-sensitive search of the file returns zero matches for the single-line literal `is private and lives under UtilitiesCS`; and the file is at or below its [P0-T14] baseline of 312 lines. -- [ ] [P2-T11] Project the score paths inside the internal four-argument BindRowsAsync at `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` lines 92-150, immediately before the `_builder.BuildRows` call at lines 132-136, using the bound root the method already normalizes at lines 107-109. Build a new score list that contains every original score UNCHANGED and, additionally, one projected `FolderScore` carrying `ArchiveStemProjection.ToDisplayStem(score.FolderPath, _boundRoot)` with the same score and probability whenever that projection differs from the original path under an ordinal comparison. The addition is what makes it safe: substitution would fix the stem-presented case and silently break the rooted-presented case (D7), and the probability index at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:224 assigns through the indexer, so duplicate keys are tolerated rather than throwing. The public three-argument overload at lines 75-82 forwards an empty root, so the projection is the identity for every caller of that overload and no existing behaviour changes there. Do not modify the row builder and do not change any public signature; re-keying the join was considered and rejected because BuildRows takes only a string list and a score sequence, so there is no correlating identity to key on without changing a public signature and every test that calls it. Acceptance: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage`, `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` and `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` all pass, and `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` and `Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` in QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs both still pass. +- [ ] [P2-T11] Project the score paths inside the internal four-argument BindRowsAsync at `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` lines 92-150, immediately before the `_builder.BuildRows` call at lines 132-136, using the bound root the method already normalizes at lines 107-109. Build a new score list that contains every original score UNCHANGED and, additionally, one projected `FolderScore` carrying `ArchiveStemProjection.ToDisplayStem(score.FolderPath, _boundRoot)!` with the same score and probability whenever that projection differs from the original path under an ordinal comparison. The null-forgiving operator carries a one-line comment giving its reason: this file opens with `#nullable enable` at line 1, [P1-T1] declares `ToDisplayStem` with a `string?` return, and `ToDisplayStem` returns null only when its own `folderPath` argument is null, which the projection loop excludes by skipping any score whose `FolderPath` is null before calling; the unsuppressed form is CS8600 or CS8604 at the `FolderScore` construction and the [P3-T4] gate promotes either to a build error. The addition is what makes it safe: substitution would fix the stem-presented case and silently break the rooted-presented case (D7), and the probability index at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:224 assigns through the indexer, so duplicate keys are tolerated rather than throwing. The public three-argument overload at lines 75-82 forwards an empty root, so the projection is the identity for every caller of that overload and no existing behaviour changes there. Do not modify the row builder and do not change any public signature; re-keying the join was considered and rejected because BuildRows takes only a string list and a score sequence, so there is no correlating identity to key on without changing a public signature and every test that calls it. Acceptance: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage`, `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` and `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` all pass, and `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` and `Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` in QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs both still pass. -- [ ] [P2-T12] Apply the AC7 zero-candidate row suppression on the Efc surface only, per the escalation branch D5 records, inside the same internal BindRowsAsync in `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`. Add a private readonly `IFolderLabelAbsenceReport` field assigned in the existing constructor body at lines 48-56 from `provider as IFolderLabelAbsenceReport`, which changes no constructor signature and therefore breaks no existing test; a `Mock` is not an `IFolderLabelAbsenceReport`, so the field is null and suppression is inert in every existing router test (D4). In the chain loop at lines 110-130, when a suggestion row's hierarchy path is non-null, its fetched chain is null, and the absence report says that hierarchy path is an absent label, record the presented text in a suppression set; a null chain arising from cancellation or from a provider fault is NOT suppressed, because those rows are not known-absent. Derive the retained presented-row list from the suppression set and pass that SAME list to both `_builder.BuildRows` at lines 132-136 and `AttachSegmentKeys` at line 137, because AttachSegmentKeys indexes the presented rows by row index at line 176 and would mis-align against an unfiltered list. Log the suppressed count at DEBUG through the `log` field declared at lines 21-23. When nothing is suppressed, pass the original list unchanged so the common path allocates nothing. Acceptance: the solution compiles; every `[TestMethod]` in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` passes; and all five #439 router tests named in [P0-T15] still pass, which is the observable proof that suppression is inert behind a mocked provider. +- [ ] [P2-T12] Apply the AC7 zero-candidate row suppression on the Efc surface only, per the escalation branch D5 records, inside the same internal BindRowsAsync in `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`. Add a private readonly `IFolderLabelAbsenceReport` field assigned in the existing constructor body at lines 48-56 from `provider as IFolderLabelAbsenceReport`, which changes no constructor signature and therefore breaks no existing test; a `Mock` is not an `IFolderLabelAbsenceReport`, so the field is null and suppression is inert in every existing router test (D4). In the chain loop at lines 110-130, when a suggestion row's hierarchy path is non-null, its fetched chain is null, and the absence report says that hierarchy path is an absent label, record the presented text in a suppression set; a null chain arising from cancellation or from a provider fault is NOT suppressed, because those rows are not known-absent. Derive the retained presented-row list from the suppression set and pass that SAME list to both `_builder.BuildRows` at lines 132-136 and `AttachSegmentKeys` at line 137, because AttachSegmentKeys indexes the presented rows by row index at line 176 and would mis-align against an unfiltered list. Log the suppressed count at DEBUG through the `log` field declared at lines 21-23. When nothing is suppressed, pass the original list unchanged so the common path allocates nothing. Acceptance: the solution compiles; every `[TestMethod]` in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` passes, including `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned`, which is the only test that executes this task's branch, and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed`, which pins the zero-candidate restriction at the router boundary; and all ten tests of the partial class `BreadcrumbBridgeRouterIssue439Tests`, across both its files, still pass, which is the observable proof that suppression is inert behind a provider mock that does not implement the absence report. - [ ] [P2-T13] Add the lazy root accessor argument to the provider construction at `QuickFiler/Controllers/EfcFormController.cs` lines 1053-1055, passing a delegate that reads the archive root from the existing application-globals accessor at call time rather than at construction time. The delegate form is mandatory: the archive-root property throws when the root is unresolvable, this construction is not inside a try, and QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242 exists because of that throw (D2). Change nothing else in this file; the raw score read at lines 1115-1117 and the four-argument router call at line 1118 stay exactly as they are, because AC6 is delivered in the router (D7) and this file cannot absorb growth. Acceptance: the solution compiles; `BindBreadcrumbRowsAsync_WhenArchiveRootThrows_ReportsOnceAndDoesNotThrow` still passes; and the file is at or below 1322 lines, that is its [P0-T14] baseline of 1320 plus the at-most-two lines the added argument costs (D11). @@ -620,7 +637,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" ``` -- [ ] [P2-T16] Run every test named in the [P1-T18] inventory and record `/evidence/regression-testing/p2-t16-pass-after.md`. Acceptance: `EXIT_CODE: 0`, `Failed: 0`, and the artifact records, for every test named in the [P1-T18] inventory, a `PASS-AFTER: ` line derived from the TRX, with the count of those lines equal to the [P1-T18] inventory count. The two runs' own totals are recorded separately as `P2-T16-TOTAL-PASSED:` and `P2-T16-TOTAL-RUN:` and are NOT asserted against the inventory count, because the filters select whole classes and therefore also run tests that were already green at the end of Phase 1. +- [ ] [P2-T16] Run every test named in the [P1-T18] inventory and record `/evidence/regression-testing/p2-t16-pass-after.md`. Acceptance: the artifact records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, both `0`, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema; `FAILED-UT: 0` and `FAILED-QFT: 0`, each read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run; and, for every test named in the [P1-T18] inventory, a `PASS-AFTER: ` line derived from the TRX, with the count of those lines equal to the [P1-T18] inventory count. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. The two runs' own totals are recorded separately as `P2-T16-TOTAL-PASSED:` and `P2-T16-TOTAL-RUN:` and are NOT asserted against the inventory count, because the filters select whole classes and therefore also run tests that were already green at the end of Phase 1. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -629,7 +646,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p2-t16-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterScoreJoinTests|FullyQualifiedName~QfcItemController_FolderHandlingTests|FullyQualifiedName~BreadcrumbBridgeRouterIssue439Tests' ``` -- [ ] [P2-T17] Run the whole UtilitiesCS.Test and QuickFiler.Test assemblies and record `/evidence/regression-testing/p2-t17-suites.md` with the derived lines `POST-UT-TOTAL:`, `POST-UT-PASSED:`, `POST-UT-FAILED:`, `POST-QFT-TOTAL:`, `POST-QFT-PASSED:`, `POST-QFT-FAILED:` and a `NEWLY-FAILING:` line listing every test failing here that was not failing in the [P0-T11] baseline. Acceptance: `NEWLY-FAILING: NONE`, `POST-UT-FAILED` is less than or equal to `BASELINE-UT-FAILED`, and `POST-QFT-FAILED` is less than or equal to `BASELINE-QFT-FAILED`. +- [ ] [P2-T17] Run the whole UtilitiesCS.Test and QuickFiler.Test assemblies and record `/evidence/regression-testing/p2-t17-suites.md` with the derived lines `POST-UT-TOTAL:`, `POST-UT-PASSED:`, `POST-UT-FAILED:`, `POST-QFT-TOTAL:`, `POST-QFT-PASSED:`, `POST-QFT-FAILED:` and a `NEWLY-FAILING:` line listing every test failing here that was not failing in the [P0-T11] baseline. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: the artifact records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema; each `POST-*-FAILED` value is read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run; `NEWLY-FAILING: NONE`; `POST-UT-FAILED` is less than or equal to `BASELINE-UT-FAILED`; and `POST-QFT-FAILED` is less than or equal to `BASELINE-QFT-FAILED`. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -656,9 +673,11 @@ $beforeStat = @(git diff --stat $BaseSha) dotnet tool run csharpier format . $after = @(git status --porcelain --untracked-files=all) $afterStat = @(git diff --stat $BaseSha) +"PATH_SETS_IDENTICAL=$(($null -eq (Compare-Object -ReferenceObject $before -DifferenceObject $after)))" +"DIFFSTAT_IDENTICAL=$(($null -eq (Compare-Object -ReferenceObject $beforeStat -DifferenceObject $afterStat)))" ``` -- [ ] [P3-T2] Run the read-only CSharpier check and record `/evidence/qa-gates/p3-t2-format-check.md` with the verbatim printed line and the derived line `FINAL-CSHARPIER-CHECKED-FILES: `. The success-case output on a clean tree is the single line of the form `Checked files in ms.` with exit 0. Record the delta against `BASELINE-CSHARPIER-CHECKED-FILES` from [P0-T8]; seven new `.cs` files are added by this plan, so a delta of 7 is the expected observation, and any other value must be explained in the artifact. Acceptance: `EXIT_CODE: 0`. The exit code is the gate here, because `check` is read-only and returns non-zero on drift. +- [ ] [P3-T2] Run the read-only CSharpier check and record `/evidence/qa-gates/p3-t2-format-check.md` with the verbatim printed line and the derived line `FINAL-CSHARPIER-CHECKED-FILES: `. The success-case output on a clean tree is the single line of the form `Checked files in ms.` with exit 0. Record the delta against `BASELINE-CSHARPIER-CHECKED-FILES` from [P0-T8]; eight new `.cs` files are added by this plan — three production and five test — so a delta of 8 is the expected observation, and any other value must be explained in the artifact. Acceptance: `EXIT_CODE: 0`. The exit code is the gate here, because `check` is read-only and returns non-zero on drift. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path @@ -678,7 +697,7 @@ msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true ``` -- [ ] [P3-T5] Run the full nine-assembly suite under `dotnet-coverage`, writing the Cobertura document to artifacts\csharp\coverage.xml, and record `/evidence/qa-gates/p3-t5-tests-coverage.md` with the derived lines `FINAL-LINES-COVERED:`, `FINAL-LINES-VALID:`, `FINAL-BRANCHES-COVERED:`, `FINAL-BRANCHES-VALID:`, `FINAL-PACKAGES-MATCHED:`, the two derived percentages, and `FINAL-TOTAL-TESTS:` / `FINAL-FAILED-TESTS:`. The four counters are aggregated by the same pinned block [P0-T12] used, over the same nine first-party package names, under the same derived coverage configuration and the same test-case filter, so the two sides are produced by one collector, one configuration, one selection and one filter. The artifacts directory is git-ignored at `.gitignore` line 57, so the document is a local tool output rather than committed evidence; the acceptance below is on-disk existence and the recorded counters, not on `git ls-files`. Acceptance: `EXIT_CODE: 0`, `FINAL-FAILED-TESTS: 0`, artifacts\csharp\coverage.xml exists, all five `FINAL-` counter lines are numeric, and `FINAL-PACKAGES-MATCHED` equals `BASELINE-PACKAGES-MATCHED` from [P0-T12]. +- [ ] [P3-T5] Run the full nine-assembly suite under `dotnet-coverage`, writing the Cobertura document to artifacts\csharp\coverage.xml, and record `/evidence/qa-gates/p3-t5-tests-coverage.md` with the derived lines `FINAL-LINES-COVERED:`, `FINAL-LINES-VALID:`, `FINAL-BRANCHES-COVERED:`, `FINAL-BRANCHES-VALID:`, `FINAL-PACKAGES-MATCHED:`, the two derived percentages, and `FINAL-TOTAL-TESTS:` / `FINAL-FAILED-TESTS:`. The four counters are aggregated by the same pinned block [P0-T12] used, over the same nine first-party package names, under the same derived coverage configuration and the same test-case filter, so the two sides are produced by one collector, one configuration, one selection and one filter. The artifacts directory is git-ignored at `.gitignore` line 57, so the document is a local tool output rather than committed evidence; the acceptance below is on-disk existence and the recorded counters, not on `git ls-files`. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. If the [P3-T6] loop restarts after this task has run, artifacts\csharp\coverage.xml is deleted before [P3-T1] is re-run, because the file is a machine-generated XML document that CSharpier would otherwise take as formatting input. Acceptance: `EXIT_CODE: 0`; `FINAL-FAILED-TESTS:` is read from the run's TRX `ResultSummary/Counters` `failed` attribute rather than from the console, because vstest prints no `Failed:` line on a fully passing run, and is less than or equal to `BASELINE-FAILED-TESTS` from [P0-T12] with a `NEWLY-FAILING:` line naming every test failing here that was not failing there, which must read `NEWLY-FAILING: NONE`; artifacts\csharp\coverage.xml exists; all five `FINAL-` counter lines are numeric; and `FINAL-PACKAGES-MATCHED` equals `BASELINE-PACKAGES-MATCHED` from [P0-T12]. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path @@ -717,11 +736,48 @@ foreach ($p in @('UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvid "=== $p" git diff --unified=0 $BaseSha -- $p } +$doc = New-Object System.Xml.XmlDocument +$doc.Load((Resolve-Path -LiteralPath 'artifacts\csharp\coverage.xml').Path) +foreach ($n in @('OutlookFolderHierarchyProvider.cs','FolderPredictor.cs','QfcItemController.FolderHandling.cs','BreadcrumbBridgeRouter.cs','EfcFormController.cs','QfcItemController.ViewerSetup.cs')) { + $map = @{} + foreach ($c in $doc.SelectNodes('//class')) { + $f = $c.GetAttribute('filename') + if (-not ($f.EndsWith('\' + $n) -or $f.EndsWith('/' + $n))) { continue } + foreach ($ln in $c.SelectNodes('.//line')) { + $num = [int]$ln.GetAttribute('number'); $h = [int]$ln.GetAttribute('hits') + if (-not $map.ContainsKey($num) -or $map[$num] -lt $h) { $map[$num] = $h } + } + } + "FILE=$n MAPPED_LINES=$($map.Count)" + foreach ($k in ($map.Keys | Sort-Object)) { " LINE=$k HITS=$($map[$k])" } +} ``` - [ ] [P3-T8] Write `/evidence/qa-gates/p3-t8-coverage-delta.md` comparing the five [P0-T12] baseline counters against the five [P3-T5] final counters. Record the comparability precondition first: `FINAL-LINES-VALID` and `BASELINE-LINES-VALID` must be compared and their relation stated, because the denominator grows when new production lines are added and the two sides are only directly comparable when it does not. When the denominators differ, compare the two derived percentages instead and state that the percentage comparison is the one used. Acceptance: the artifact records baseline coverage, post-change coverage, and the changed-line determination from [P3-T7], and states explicitly whether the repository-wide first-party line percentage decreased. -- [ ] [P3-T9] Write `/evidence/qa-gates/p3-t9-new-type-coverage.md` recording line and branch coverage for the two new production types, read from artifacts\csharp\coverage.xml by selecting the class elements whose `filename` attribute ends with a directory separator followed by `ArchiveStemProjection.cs` or `ArchiveChainProjection.cs`. The repository unit-test policy requires new modules, classes and methods to target at least 90 percent coverage. Acceptance: the artifact records a numeric line-coverage percentage for each of the two files, both are at or above 90 percent, and the class-element counts the figures were derived from are recorded so a zero-element selection is visible rather than silent. +- [ ] [P3-T9] Write `/evidence/qa-gates/p3-t9-new-type-coverage.md` recording line and branch coverage for the two new production types, read from artifacts\csharp\coverage.xml by selecting the class elements whose `filename` attribute ends with a directory separator followed by `ArchiveStemProjection.cs` or `ArchiveChainProjection.cs`. The repository unit-test policy requires new modules, classes and methods to target at least 90 percent coverage. Acceptance: the block prints one `FILE=` line per new type; `CLASS_ELEMENTS` is greater than zero on both, so a zero-element selection is visible rather than silent; and `LINES_COVERED` divided by `LINES_VALID` is at or above 0.90 for each of the two files. + +```powershell +$doc = New-Object System.Xml.XmlDocument +$doc.Load((Resolve-Path -LiteralPath 'artifacts\csharp\coverage.xml').Path) +foreach ($n in @('ArchiveStemProjection.cs','ArchiveChainProjection.cs')) { + $elems = 0; $map = @{}; $bc = 0; $bv = 0 + foreach ($c in $doc.SelectNodes('//class')) { + $f = $c.GetAttribute('filename') + if (-not ($f.EndsWith('\' + $n) -or $f.EndsWith('/' + $n))) { continue } + $elems++ + foreach ($ln in $c.SelectNodes('.//line')) { + $num = [int]$ln.GetAttribute('number'); $h = [int]$ln.GetAttribute('hits') + if (-not $map.ContainsKey($num) -or $map[$num] -lt $h) { $map[$num] = $h } + $cc = $ln.GetAttribute('condition-coverage') + if ($cc -match '\((\d+)/(\d+)\)') { $bc += [int]$Matches[1]; $bv += [int]$Matches[2] } + } + } + $lv = $map.Count + $lc = @($map.Values | Where-Object { $_ -gt 0 }).Count + "FILE=$n CLASS_ELEMENTS=$elems LINES_COVERED=$lc LINES_VALID=$lv BRANCHES_COVERED=$bc BRANCHES_VALID=$bv" +} +``` - [ ] [P3-T10] Record the post-format line count of every file this plan edited or created into `/evidence/qa-gates/p3-t10-sizes.md`, one ` = ` line per file alongside its [P0-T14] baseline, keeping the four project files under the same `PROJECT-FILE (exempt):` heading (R8). This audit runs AFTER the final format because CSharpier can change line counts (R9). Acceptance: `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is at or below 500; `QuickFiler/Controllers/EfcFormController.cs` is at or below 1322; `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` is at or below 1003; every other listed `.cs` file is at or below 500; the exempt project-file counts are recorded but not asserted against the ceiling; and the artifact states the smallest remaining headroom across all listed `.cs` files together with the three disclosed pre-existing over-ceiling files and their budgets (D11). @@ -734,10 +790,10 @@ git diff --name-only $BaseSha -- '*.cs' '*.csproj' git status --porcelain --untracked-files=all -- '*.cs' '*.csproj' ``` -- [ ] [P3-T12] Write `/evidence/qa-gates/p3-t12-ac8-verification.md` recording the AC8 finding as verified evidence rather than as a fix, per `spec.md` decision D-C. The artifact must re-run and record the repository-wide negative search below over the six product projects, and must record the five traced transformations and why each leaves a leading underscore unchanged: the verbatim splitter splits on path separators with empty entries removed and inserts nothing; the JSON serializer escapes only the double quote, the backslash and control characters, and the non-indenting format adds no whitespace inside string values; the QuickFiler page assigns segment text through the DOM textContent property, which performs no entity decoding and no transformation; the Efc page encodes ampersand, less-than, greater-than and double-quote only, so underscore and space pass through and no non-breaking space is emitted; and neither stylesheet contains letter-spacing, word-spacing, text-transform, a first-letter pseudo-element or word-break. It must also record that the WinForms mnemonic prefix character is the ampersand and not the underscore, so no combo-box or owner-draw path can be responsible. The conclusion is recorded as a definite finding: the reported space after the leading underscore was a transcription artifact, the renderer is correct, and a renderer change would be a defect. No file is edited by this task. Acceptance: the search records zero matching files, the artifact states the conclusion as a definite finding, and the artifact records `FILES-CHANGED-FOR-AC8: 0`. +- [ ] [P3-T12] Write `/evidence/qa-gates/p3-t12-ac8-verification.md` recording the AC8 finding as verified evidence rather than as a fix, per `spec.md` decision D-C. The artifact must re-run and record the repository-wide negative search below over the six product projects, and must record the five traced transformations and why each leaves a leading underscore unchanged: the verbatim splitter splits on path separators with empty entries removed and inserts nothing; the JSON serializer escapes only the double quote, the backslash and control characters, and the non-indenting format adds no whitespace inside string values; the QuickFiler page assigns segment text through the DOM textContent property, which performs no entity decoding and no transformation; the Efc page encodes ampersand, less-than, greater-than and double-quote only, so underscore and space pass through and no non-breaking space is emitted; and neither stylesheet contains letter-spacing, word-spacing, text-transform, a first-letter pseudo-element or word-break. It must also record that the WinForms mnemonic prefix character is the ampersand and not the underscore, so no combo-box or owner-draw path can be responsible. The conclusion is recorded as a definite finding: the reported space after the leading underscore was a transcription artifact, the renderer is correct, and a renderer change would be a defect. No file is edited by this task. Acceptance: the artifact records `ExpectedExitCode: 1` and `EXIT_CODE: 1`, because `git grep` exits 1 when it matches nothing and zero matches is this task's SUCCESS outcome, so an artifact omitting the expectation would normalise a passing gate to `fail`; the artifact records that the command produced no output lines; it states the conclusion as a definite finding; and it records `FILES-CHANGED-FOR-AC8: 0`. The regex is single-quoted with the inner single quote doubled, because PowerShell does not treat a backslash as an escape inside a double-quoted string and the double-quoted form of this pattern does not parse. ```powershell -git grep -n -E "Replace\(\"_|Replace\('_|letter-spacing|text-transform|first-letter|word-break|word-spacing" -- "UtilitiesCS/*.cs" "UtilitiesCS/*.html" "UtilitiesCS/*.css" "QuickFiler/*.cs" "QuickFiler/*.html" "QuickFiler/*.css" "ToDoModel/*.cs" "TaskMaster/*.cs" "Tags/*.cs" "TaskVisualization/*.cs" +git grep -n -E 'Replace\(["'']_|letter-spacing|text-transform|first-letter|word-break|word-spacing' -- "UtilitiesCS/*.cs" "UtilitiesCS/*.html" "UtilitiesCS/*.css" "QuickFiler/*.cs" "QuickFiler/*.html" "QuickFiler/*.css" "ToDoModel/*.cs" "TaskMaster/*.cs" "Tags/*.cs" "TaskVisualization/*.cs" ``` - [ ] [P3-T13] Check off AC1 in `spec.md` line 834 by changing its `- [ ]` to `- [x]`, citing the [P1-T16] fail-before artifact and the [P2-T16] pass-after artifact and naming the passing tests `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` and `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`. Acceptance: exactly one AC checkbox changes in this task and the AC1 line carries `- [x]`. @@ -752,9 +808,9 @@ git grep -n -E "Replace\(\"_|Replace\('_|letter-spacing|text-transform|first-let - [ ] [P3-T18] Check off AC6 in `spec.md` line 839, citing [P1-T17] and [P2-T16] and the passing `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, and recording the two deviations from the spec's own wording: the projection is applied in the Efc router rather than at the controller call site named by the criterion, because the controller is 1320 lines and cannot absorb growth while the router already normalizes the bound root; and the projected score is ADDED alongside the raw score rather than substituted for it, per D7. Acceptance: exactly one AC checkbox changes in this task and the AC6 line carries `- [x]`. -- [ ] [P3-T19] Check off AC7 in `spec.md` line 840, citing the passing `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` from [P2-T16], and recording three things: that the filtered branch is taken restricted to the zero-candidate case per decision D-B; that "per session" is realized as "per provider instance" and is enforced by a thread-safe per-instance set rather than a static one; and that the escalation branch D5 records was taken, so row suppression is delivered on the Efc surface only while the logging half is delivered on both surfaces. Acceptance: exactly one AC checkbox changes in this task, the AC7 line carries `- [x]`, and the check-off note names the escalation deviation. +- [ ] [P3-T19] Check off AC7 in `spec.md` line 840, citing the passing `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` from [P2-T16] for the logging half, and the passing `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned` and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed` from [P2-T16] for the row-suppression half, which are the only two tests in this plan that execute the [P2-T12] branch, and recording three things: that the filtered branch is taken restricted to the zero-candidate case per decision D-B; that "per session" is realized as "per provider instance" and is enforced by a thread-safe per-instance set rather than a static one; and that the escalation branch D5 records was taken, so row suppression is delivered on the Efc surface only while the logging half is delivered on both surfaces. Acceptance: exactly one AC checkbox changes in this task, the AC7 line carries `- [x]`, and the check-off note names the escalation deviation. -- [ ] [P3-T20] Check off AC8 in `spec.md` line 841, citing `/evidence/qa-gates/p3-t12-ac8-verification.md` and recording that AC8 is satisfied by the verified finding that the renderer does not alter a leading underscore, that no code change was made, and that a renderer change would have been a defect. Acceptance: exactly one AC checkbox changes in this task, the AC8 line carries `- [x]`, and no file under `QuickFiler/Resources` appears in the [P3-T11] scope enumeration. +- [ ] [P3-T20] Check off AC8 in `spec.md` line 841, citing `/evidence/qa-gates/p3-t12-ac8-verification.md` and recording that AC8 is satisfied by the verified finding that the renderer does not alter a leading underscore, that no code change was made, and that a renderer change would have been a defect. Acceptance: exactly one AC checkbox changes in this task, the AC8 line carries `- [x]`, and no file under QuickFiler/Resources appears in the [P3-T11] scope enumeration. - [ ] [P3-T21] Update the `spec.md` Status line to `Implemented` and add an "Outcome" note under Rollout & Follow-up recording the four deviations this plan makes from the spec's own prose, each with its reason: AC7 row suppression is delivered on the Efc surface only, because the QuickFiler presented row set is composed solely inside the sibling-owned bridge router, which decision D-B forbids this item from editing (D5); the AC6 score projection is additive rather than substitutive, so the rooted-presented-text join cannot regress (D7); the two #439 Efc router test files listed in the Write Set carry no hunk, because every test in both drives a mocked provider below the trim boundary and editing their shared chain fixture would break unrelated #614 boundary tests (D8); and the AC7 absence classification is published through a new small public interface declared in the provider's own file rather than through a fourth member on the shared hierarchy contract, which net48 cannot add without breaking every implementer and every strict mock (D4). Acceptance: the Status line reads `Implemented` and all four deviations are recorded by name. @@ -795,11 +851,27 @@ seven findings that changed the plan away from what the spec and the research al unsatisfiable, FolderPredictor.cs is 1003 and shrinks by at least eight lines by derivation, and OutlookFolderHierarchyProviderTests.cs is 479 with only 21 lines of headroom for its retarget. +Round 2 (revision pass against the orchestrator-adjudicated preflight delta). Every citation that this pass's edits touch +was re-derived directly against this worktree in this pass, together with the sibling lines and tests in the same region: + +- QuickFiler.Test/QuickFiler.Test.csproj. No `` element occurs anywhere in the file; `v4.8.1` is at line 18. Sibling re-check: UtilitiesCS.Test/UtilitiesCS.Test.csproj carries `v4.8.1` at line 17 and `Latest` at line 18, so the two test projects differ only in the language-version element. This is the premise of the [P1-T11] C# 7.3 authoring constraint. +- QuickFiler.Test, project-wide. Zero files carry a `#nullable` directive. Sibling re-check: the only two `.As<` uses in the whole project are `.As()` at QuickFiler.Test/Viewers/BreadcrumbDropDownLifecycleCoverageTests.cs:301 and at QuickFiler.Test/Viewers/BreadcrumbPopupBoundaryCoverageTests.Part2.cs:373, both outside Controllers, so [P1-T11]'s `.As()` reaches no existing router test and D4's inertness claim survives the new tests. +- QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs. Six `[TestMethod]` attributes at lines 20, 118, 168, 257, 302 and 379, with their methods at 21, 119, 169, 258, 303 and 380, and six strict provider mocks at 30, 125, 178, 261, 311 and 385. The previously omitted pair is Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome at line 169 with its mock at line 178, and that is the test which pins today's null-chain selectable-fallback rendering — the exact path [P2-T12] modifies. +- QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs. Four `[TestMethod]` attributes at lines 14, 63, 110 and 177, with Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget at 15, Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget at 64, Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget at 111 and Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild at 178, and four strict provider mocks at 21, 70, 118 and 186. Ten tests across the two files, not five. +- UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs, UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs and QuickFiler/Controllers/BreadcrumbBridgeRouter.cs each open with `#nullable enable` at line 1. Sibling re-check: QuickFiler/Controllers/QfcItemController.FolderHandling.cs carries no `#nullable` directive, which is why [P2-T10]'s delegation needs no suppression while [P2-T7]'s and [P2-T11]'s do. +- UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs. `TryMakeArchiveRelative` declares `string fullPath` at line 107 and `string archiveRoot` at line 108, both NON-nullable, inside a file that opens with `#nullable enable` at line 1. This is the sibling finding produced by this pass: the `?` annotations that B3a and B3b add to the two new helpers cannot be passed straight through to the contract, so [P2-T1] and [P2-T2] each gained the explicit leading null guard the compiler requires, and [P2-T7] and [P2-T11] each gained a reasoned null-forgiving operator because their host members declare non-nullable `string` returns. +- UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:848. `private string ProjectSuggestionPath(string folderPath)` — non-nullable return and non-nullable parameter, with its two call sites at lines 810 and 842 unchanged, which is why the delegation is suppressed rather than the signature widened. +- QuickFiler/Controllers/QfcItemController.FolderHandling.cs:272. `internal static string ProjectPredeterminedFolder(string folderPath, string archiveRootPath)` with its own null guard at line 274, in a file with no nullable context. +- UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs. 479 lines. GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments occupies lines 72-91, a 20-line body, with the root-to-leaf assertion at 84-87 asserting `\Root`, `\Root\Clients` and `\Root\Clients\Acme`, and the provider constructed at 76-78 with a single argument. Thirteen provider constructions at 76, 98, 118, 139, 157, 175, 193, 218, 240, 269, 299, 316 and 338, the single-null-argument one at 316. A same-shape companion case plus its blank separator would consume the file's entire 21-line headroom, which is the derivation behind [P1-T13]'s relocation of the off-switch pin and its 483-line budget. +- Sibling consequences of that relocation, re-checked and corrected in this pass: [P1-T8] already declares GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain, so the pin lands on an already-planned test rather than an orphan reference; [P2-T5]'s acceptance said "the two retargeted provider tests from [P1-T13]" and now names the single one; D9's companion-case sentence was corrected; and the Write Set row's budget changed from +21 to +4. +- Record-hygiene correction found by re-reading the bounded record itself in this pass: the round 1 record carried a CITATION line whose path token contained a space (the ToDoModel email-utilities test file). That contradicted this plan's own R12 and D10 convention, which requires both space-containing paths to be named in words only, and it is not a well-formed path token. The line was removed; the fact it carried is retained in D9, which names that file in words and gives its two GetOlSubpath assertion ranges. +- Sibling consequence of the [P1-T11] test-count change, re-checked in this pass: the task's former closing claim that "every provider interaction is Moq-supplied, so the AC7 suppression path is inert in this file by construction" is no longer true of this file and was replaced by the narrower claim the new tests actually support, that no OTHER router test is reached. [P2-T12]'s acceptance and [P3-T19]'s check-off citation were both updated to name the two new tests, and the AC7 row of AC-MAPPING now carries them. + Delta self-check: this document's own prose was checked against the rules it enforces. Every backticked path with a directory separator is a Write Set path taken from spec.md lines 645-702; every sibling-owned, precedent, comparison and out-of-scope path is written in bare prose; both space-containing paths are named in words only and neither is in this item's footprint; no asserted token carries an angle bracket, a dollar-brace, a dollar-paren or a percent sign; every -`git diff` carries an explicit ref operand bound inside its own block together with a staging or porcelain companion; the +git diff invocation in this plan carries an explicit ref operand bound inside its own block together with a staging or porcelain companion; the two write-mode commands (the formatter and the repo-local SDK installer) each record an observation beyond their exit code; the coverage aggregation prints the exact line its acceptance reads; and no task's acceptance depends on an artifact a later task writes — [P0-T13] and [P0-T14] precede every consumer, [P1-T18] precedes [P2-T16], [P0-T12] precedes [P3-T8], @@ -809,32 +881,31 @@ PLANNER-INTERNAL-REVIEW: PASS CITATION-TO-TREE: PASS AC-TRACEABILITY: PASS SCOPE-BOUNDARY: PASS -CITATION: UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs | 147 lines; TryMakeArchiveRelative at lines 106-145; whitespace-root guard at line 113; trailing-separator trim at line 118; equality returning true with an empty stem at lines 124-127; StartsWith prefix test at line 131; separator-boundary test at lines 137-141; stem leading-separator trim at line 143 -CITATION: UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs | 141 lines; log4net ILog at lines 17-19; single constructor at lines 28-31; GetAncestorChainAsync at lines 34-42; ResolveLeafKeyAsync at lines 56-80 with the exact-path early return at lines 74-77; private static ResolveByUniqueSuffix at lines 90-114 with the two-cause emission at lines 108-112 +CITATION: UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs | 147 lines; #nullable enable at line 1; TryMakeArchiveRelative at lines 106-145 declaring NON-nullable string fullPath at line 107 and NON-nullable string archiveRoot at line 108 with out string stem at line 109; whitespace-root guard at line 113; trailing-separator trim at line 118; equality returning true with an empty stem at lines 124-127; StartsWith prefix test at line 131; separator-boundary test at lines 137-141; stem leading-separator trim at line 143 +CITATION: UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs | 141 lines; #nullable enable at line 1; log4net ILog at lines 17-19; single constructor at lines 28-31; GetAncestorChainAsync at lines 34-42; ResolveLeafKeyAsync at lines 56-80 with the exact-path early return at lines 74-77; private static ResolveByUniqueSuffix at lines 90-114 with the two-cause emission at lines 108-112 CITATION: UtilitiesCS/OutlookObjects/Folder/IFolderHierarchyProvider.cs | 65 lines; exactly three members at lines 31-34, 46-49 and 60-63 -CITATION: UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs | 1003 lines; text-parity XML doc at lines 233-242; AddRecents at lines 788-795 with the unprojected AddRange at line 793; AddSuggestions at lines 807-811; AddSuggestionRows at lines 835-846; ProjectSuggestionPath at lines 848-861 with the length guard at line 858; AddRecentRows at lines 866-882 with the unprojected row at line 879; GetOlSubpath at lines 953-971 with the include-children true branch at lines 955-965 +CITATION: UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs | 1003 lines; #nullable enable at line 1; private string ProjectSuggestionPath(string folderPath) declared with a NON-nullable return and a NON-nullable parameter at line 848; text-parity XML doc at lines 233-242; AddRecents at lines 788-795 with the unprojected AddRange at line 793; AddSuggestions at lines 807-811; AddSuggestionRows at lines 835-846; ProjectSuggestionPath at lines 848-861 with the length guard at line 858; AddRecentRows at lines 866-882 with the unprojected row at line 879; GetOlSubpath at lines 953-971 with the include-children true branch at lines 955-965 CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs | 238 lines; empty-chain single-segment fallback at lines 123-131; presented-text probability lookup at lines 133-135; MapSegments at lines 178-208; BuildProbabilityIndex at lines 210-229 assigning through the indexer at line 224 CITATION: UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbSegment.cs | 54 lines; four-argument constructor at lines 29-40; FolderPath property at line 49 CITATION: UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs | 489 lines, sibling-owned; SetSuggestionsAsync at lines 29-97 composing the presented row set at lines 42-86 and swapping it under the shared lock at lines 88-96; shared _sync at line 15 and _suggestionGeneration at line 16 CITATION: UtilitiesCS/Properties/AssemblyInfo.cs | InternalsVisibleTo("UtilitiesCS.Test") at line 19 -CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.cs | 304 lines; log field at lines 21-23; _boundRoot at line 35; constructor at lines 41-56; public three-argument BindRowsAsync at lines 75-82 forwarding an empty root at line 81; internal four-argument BindRowsAsync at lines 92-150 with root normalization at lines 107-109, chain loop at lines 110-130, BuildRows call at lines 132-136 and AttachSegmentKeys call at line 137; ToHierarchyPath at lines 152-167; AttachSegmentKeys indexing presented rows at line 176 +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.cs | 304 lines; #nullable enable at line 1; log field at lines 21-23; _boundRoot at line 35; constructor at lines 41-56; public three-argument BindRowsAsync at lines 75-82 forwarding an empty root at line 81; internal four-argument BindRowsAsync at lines 92-150 with root normalization at lines 107-109, chain loop at lines 110-130, BuildRows call at lines 132-136 and AttachSegmentKeys call at line 137; ToHierarchyPath at lines 152-167; AttachSegmentKeys indexing presented rows at line 176 CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | 221 lines; FetchChainAsync at lines 50-81 returning null on a null key at lines 61-64 and on both catch arms at lines 68-80 CITATION: QuickFiler/Controllers/EfcFormController.cs | 1320 lines; ConfigureBreadcrumbControl at lines 1047-1067 with the provider construction at lines 1053-1055; BindBreadcrumbRowsAsync at lines 1111-1128 with the raw score read at lines 1115-1117 and the four-argument router call at line 1118 CITATION: QuickFiler/Controllers/QfcItemController.ViewerSetup.cs | exactly 500 lines; usings at lines 1-22; internal partial class at line 26; EnsureBreadcrumbPipeline comment at lines 132-136, ExcludeFromCodeCoverage attribute at line 137, member at lines 138-163, provider construction at lines 147-149 -CITATION: QuickFiler/Controllers/QfcItemController.FolderHandling.cs | 312 lines; AssignFolderComboBox at lines 191-250 with the row-model hand-off at lines 212 and 221 and the projection call at lines 231-234; duplication-rationale comment at lines 223-230; ProjectPredeterminedFolder XML doc at lines 252-271 and body at lines 272-285 +CITATION: QuickFiler/Controllers/QfcItemController.FolderHandling.cs | 312 lines; NO #nullable directive anywhere in the file; internal static string ProjectPredeterminedFolder(string folderPath, string archiveRootPath) at line 272 with its own null guard at line 274; AssignFolderComboBox at lines 191-250 with the row-model hand-off at lines 212 and 221 and the projection call at lines 231-234; duplication-rationale comment at lines 223-230; ProjectPredeterminedFolder XML doc at lines 252-271 and body at lines 272-285 CITATION: UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs | 479 lines; namespace at line 11; GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments at lines 72-91 with its root-to-leaf assertion at lines 84-87; 13 provider constructions at lines 76, 98, 118, 139, 157, 175, 193, 218, 240, 269, 299, 316 and 338; single-null-argument construction at line 316 CITATION: UtilitiesCS.Test/OutlookObjects/Folder/FolderHierarchyProviderAdapterTests.cs | 258 lines; 6 provider constructions at lines 98, 122, 142, 170, 185 and 202 CITATION: UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs | 1066 lines, pre-existing over-ceiling and untouched; recents test at lines 249-265 with relative recents at line 255; FolderArray recents test at lines 165-189 with the relative recent at line 175; Issue609 projection tests at lines 191-247; GetOlSubpath assertions at lines 577-591 CITATION: UtilitiesCS.Test/OutlookObjects/Folder/FolderRowTests.cs | text-parity test at lines 29-70 with the relative recent at line 39; recents mock helper at lines 246-258 -CITATION: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs | 455 lines; strict provider mocks at lines 30, 125, 261, 311 and 385; lineage test at lines 20-116 with the archive-root index assertions at lines 109 and 113; rooted-target test at lines 118-166 binding a rooted score at line 149; boundary test at lines 302-377; slash-only-root test at lines 379-427 asserting the archive root at line 426; shared Chain helper at lines 434-448 emitting the leading Archive segment at line 444 -CITATION: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs | 253 lines; strict provider mocks at lines 21, 70, 118 and 186; four uses of the shared Chain helper at lines 33, 82, 130 and 196 +CITATION: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs | 455 lines; six strict provider mocks at lines 30, 125, 178, 261, 311 and 385, one per test method at lines 21, 119, 169, 258, 303 and 380; lineage test at lines 20-116 with the archive-root index assertions at lines 109 and 113; rooted-target test at lines 118-166 binding a rooted score at line 149; boundary test at lines 302-377; slash-only-root test at lines 379-427 asserting the archive root at line 426; shared Chain helper at lines 434-448 emitting the leading Archive segment at line 444 +CITATION: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs | 253 lines; four [TestMethod] attributes at lines 14, 63, 110 and 177 with Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget at line 15, Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget at line 64, Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget at line 111 and Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild at line 178; strict provider mocks at lines 21, 70, 118 and 186; four uses of the shared Chain helper at lines 33, 82, 130 and 196 CITATION: QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs | 354 lines; namespace at line 10; partial class QfcItemController_FolderHandlingTests at line 21; ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection at lines 212-243 with the empty-root one-separator assertion at lines 219-226 CITATION: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs | 358 lines; namespace at line 16; rooted/relative pair bind at lines 254-259 -CITATION: ToDoModel.Test/Email Utilities/FolderHandlerTests_Written.cs | GetOlSubpath assertions at lines 35-50 and 52-67, both passing a separator-terminated proper prefix CITATION: UtilitiesCS/UtilitiesCS.csproj | ArchiveStemContract.cs Compile Include at line 623; OutlookFolderHierarchyProvider.cs at line 640; FolderPredictor.cs at line 808; EnsureNuGetPackageBuildImports Error at line 1293; Analyzer Include block at lines 1301-1310 naming Meziantou.Analyzer 3.0.203 and Roslynator.Analyzers 5.0.0 -CITATION: UtilitiesCS.Test/UtilitiesCS.Test.csproj | folder-test Compile Include block at lines 276-307 with OutlookFolderHierarchyProviderTests.cs at line 304; EnsureNuGetPackageBuildImports Error at line 946 +CITATION: UtilitiesCS.Test/UtilitiesCS.Test.csproj | TargetFrameworkVersion v4.8.1 at line 17 and LangVersion Latest at line 18; folder-test Compile Include block at lines 276-307 with OutlookFolderHierarchyProviderTests.cs at line 304; EnsureNuGetPackageBuildImports Error at line 946 CITATION: QuickFiler/QuickFiler.csproj | BreadcrumbBridgeRouter.cs Compile Include at line 291; QfcItemController.ViewerSetup.cs at line 335; QfcItemController.FolderHandling.cs at line 338; EnsureNuGetPackageBuildImports Error at line 586 -CITATION: QuickFiler.Test/QuickFiler.Test.csproj | BreadcrumbBridgeRouterIssue439Tests.cs Compile Include at line 64; QfcItemController.FolderHandlingTests.Part2.cs at line 182; EnsureNuGetPackageBuildImports Error at line 501 +CITATION: QuickFiler.Test/QuickFiler.Test.csproj | TargetFrameworkVersion v4.8.1 at line 18 and NO LangVersion element anywhere in the file; BreadcrumbBridgeRouterIssue439Tests.cs Compile Include at line 64; QfcItemController.FolderHandlingTests.Part2.cs at line 182; EnsureNuGetPackageBuildImports Error at line 501 CITATION: global.json | SDK 8.0.205 at line 3; paths ".dotnet-sdk" and "$host$" at lines 6-9; repo-local install-script error message at line 10 CITATION: .csharpierignore | evidence exclusion at line 4; cobertura at line 5; trx at line 8; project-file exclusion rationale at lines 9-14 CITATION: .gitignore | test-results bracket class at line 39; artifacts/ at line 57; coverage/* at line 144 @@ -849,9 +920,11 @@ AC-MAPPING: AC3 | IMPLEMENTATION: P2-T5, P2-T11 | TESTS: P1-T11 | EVIDENCE: /evidence/regression-testing/p1-t17-qft-fail.md and /evidence/regression-testing/p2-t16-pass-after.md AC-MAPPING: AC5 | IMPLEMENTATION: P2-T8 | TESTS: P1-T9 | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p2-t16-pass-after.md AC-MAPPING: AC6 | IMPLEMENTATION: P2-T11 | TESTS: P1-T11 | EVIDENCE: /evidence/regression-testing/p1-t17-qft-fail.md and /evidence/regression-testing/p2-t16-pass-after.md -AC-MAPPING: AC7 | IMPLEMENTATION: P2-T6, P2-T12 | TESTS: P1-T8 | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p2-t16-pass-after.md +AC-MAPPING: AC7 | IMPLEMENTATION: P2-T6, P2-T12 | TESTS: P1-T8 for the logging half and P1-T11 for the row-suppression half, specifically BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned and BindRowsAsync_AmbiguousLabel_IsNotSuppressed | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p1-t17-qft-fail.md and /evidence/regression-testing/p2-t16-pass-after.md AC-MAPPING: AC8 | IMPLEMENTATION: P3-T12 | TESTS: P3-T12 | EVIDENCE: /evidence/qa-gates/p3-t12-ac8-verification.md UNRESOLVED-GAPS: NONE DIRECTIVE: PREFLIGHT VALIDATION ONLY -PREFLIGHT: REQUESTED — validation-only preflight has NOT been run by this planner, because no atomic-executor delegation tool and no MCP plan validator are present in this planner's tool surface. The orchestrator must obtain one of the two exact signals, `PREFLIGHT: ALL CLEAR` or `PREFLIGHT: REVISIONS REQUIRED`, and a passing `mcp__drm-copilot__validate_orchestration_artifacts` run with `artifact_type: "plan"` before execution begins. This plan is not self-approved. -CONVERGENCE: FURTHER ROUNDS LIKELY — three decisions in this plan depart from the spec's literal wording and a reviewer may want them re-argued or re-scoped: the D5 escalation to Efc-only row suppression, the D8 finding that the two #439 Efc router test files require no hunk, and the D7 additive rather than substitutive score projection. Each is recorded with its re-derived evidence and its consequence, but each is a judgment the reviewer is entitled to reverse, and reversing any of them changes task text rather than plan structure. +DIRECTIVE NOTE: the line below is the mechanical handoff signal. It is not a discovered defect in this revision; clearance is outstanding because validation-only preflight has NOT been run by this planner, no atomic-executor delegation tool and no MCP plan validator being present in this planner's tool surface. +PREFLIGHT: REVISIONS REQUIRED +The orchestrator must obtain a genuine `PREFLIGHT: ALL CLEAR` from atomic-executor under `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, and a passing `mcp__drm-copilot__validate_orchestration_artifacts` run with `artifact_type: "plan"`, before execution begins. This plan is not self-approved. +CONVERGENCE: NO FURTHER ROUNDS EXPECTED — every accepted item of the round 1 delta is applied, the two narrowed items are applied in their narrowed form, and the one refuted item is left unapplied as instructed. The three judgment calls the previous round flagged (the D5 escalation to Efc-only row suppression, the D8 no-hunk finding on the two #439 Efc router test files, and the D7 additive score projection) were each re-checked by the reviewer against the tree in round 1 and confirmed, so they are no longer open. The only new material introduced in this pass is the nullable-narrowing consequence of the accepted annotation items, which is closed inside the four tasks that consume the annotated members. From 46df133954f0cdb2fcc5e02a75ce54867f4d8274 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Sun, 6 Sep 2026 23:52:41 -0400 Subject: [PATCH 06/13] docs(799): record the adjudicated preflight round 2 delta The confirming round returned revisions required with four blocking findings, all local corrections to acceptance clauses and one task body. All four are accepted; none is rejected or narrowed. Two were verified directly against the tree: a relocation task demanded zero matches for an identifier whose call site sits outside the moved range, and the fifth consumer of a newly nullable return was missed when the previous round narrowed its two siblings. One finding is a defect the orchestrator introduced, by accepting a replacement clause in round one that asserts an observation no scheduled command in that phase produces. It is recorded as such rather than attributed elsewhere. Co-Authored-By: Claude Fable 5.1 --- .../evidence/other/preflight-r2-delta.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/other/preflight-r2-delta.md diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/other/preflight-r2-delta.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/other/preflight-r2-delta.md new file mode 100644 index 000000000..11b376541 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/other/preflight-r2-delta.md @@ -0,0 +1,50 @@ +# Preflight round 2 delta, adjudicated by the orchestrator + +Timestamp: 2026-09-07T00-20 + +Reviewer signal: `PREFLIGHT: REVISIONS REQUIRED` with `CONVERGENCE: FURTHER ROUNDS LIKELY`. +Four blocking findings, all local corrections to acceptance clauses and one task body. No phase, task +ordering, Write Set entry or design decision changes. + +## Adjudication + +All four blocking findings are ACCEPTED. Unlike round 1, none is rejected and none is narrowed. The +reviewer's round-1 runtime claims needed adjudication because it could not execute anything; these +four are all decidable by reading, and the orchestrator verified the two most consequential premises +directly against the tree. + +- B13 verified. `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` contains + `EnsureBreadcrumbPipeline();` at line 112 and `internal void EnsureBreadcrumbPipeline()` at line + 138. The relocation task moves lines 132 to 163 only, so the call at line 112 survives and an + acceptance demanding zero matches for the bare identifier can never be satisfied. +- B14 verified. `UtilitiesCS/OutlookObjects/Folder/FolderRow.cs` opens with `#nullable enable` at + line 1 and declares `public FolderRow(string text, FolderRowKind kind, FolderScore? score)` at line + 42, a non-nullable first parameter. Passing the newly nullable projection result there is CS8604, + which the nullable gate promotes to an error. +- B15 accepted. The third acceptance conjunct of the assets task cannot fail: the plan's own scope + rule restricts the enumeration it reads to a source pathspec, and the resources directory contains + no file matching that pathspec, so the conjunct is true for every possible execution. +- B16 accepted, and it is a defect the orchestrator introduced. The clause was appended in round 1 as + part of the accepted B2 replacement text. It asserts an observation that no scheduled command in + that phase produces, because the only build in that phase deliberately runs without the gate + switches. The correction moves the proof onto two diagnostic counts recorded by that build and + re-proved under enforcement in the final phase. + +## Non-blocking items + +m5 and m6 are accepted as written. m8 requires no change: it reports a count in the forwarded prose, +not in the plan. + +m7 is accepted with a different remedy than the reviewer proposed. Rather than rewording the trailing +signal line, the plan file should carry no line matching the preflight signal vocabulary at all. +Clearance is the executor's return recorded in the orchestrator checkpoint; a signal-shaped line +inside the plan file is a second, unmaintained assertion of the same fact and will be stale the moment +clearance is granted. + +## Round count + +This is the third preflight round against a two-round target. The overrun is legitimate rather than a +review failure: each round returned a complete enumeration rather than one defect at a time, and the +round-2 findings are in regions that only came into existence when the round-1 delta was applied. No +prior pass could have observed them. B16 in particular is a defect created by the round-1 delta +itself. From ccecda72dc6aec9ac3a436a8238245f257838f93 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Mon, 7 Sep 2026 00:01:11 -0400 Subject: [PATCH 07/13] docs(799): apply the adjudicated preflight round 2 delta to the plan Closes the four blocking findings from the confirming review. A relocation task no longer demands zero matches for an identifier whose call site sits outside the moved range. The fifth consumer of the newly nullable projection return now carries the same reasoned suppression its siblings received. An assets check now derives its result from an anchored diff with a porcelain companion instead of asserting an absence that was true for every possible execution. A clause asserting an observation no scheduled command produced now reads two diagnostic counts the phase-one build actually emits. The plan file no longer carries any line matching the preflight or convergence signal vocabulary. Clearance is the executor return recorded in the orchestrator checkpoint, not an unmaintained assertion inside the plan. The plan validator passes with no warnings. Co-Authored-By: Claude Fable 5.1 --- .../plan.2026-09-06T22-01.md | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md index b28d28b86..c16d60ea9 100644 --- a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md @@ -3,9 +3,9 @@ - **Issue:** #799 - **Parent (optional):** none - **Owner:** drmoisan -- **Last Updated:** 2026-09-06T22-01 -- **Status:** Ready for preflight — revision round 2 (orchestrator-adjudicated round 1 delta applied in place) -- **Version:** 1.2 +- **Last Updated:** 2026-09-06T23-59 +- **Status:** Ready for preflight — revision round 3 (orchestrator-adjudicated round 2 delta applied in place) +- **Version:** 1.3 - **Work Mode:** full-bug (resolved from `issue.md` line 12 and `spec.md` line 9) - **Language in scope:** C# only (UtilitiesCS, QuickFiler, UtilitiesCS.Test, QuickFiler.Test; four legacy non-SDK projects with explicit Compile Include items and no globbing) - **Authoritative AC source:** `spec.md`, section "Acceptance Criteria", lines 834-841, AC1 through AC8. There is no user-story.md in this feature folder and none is required. @@ -549,9 +549,9 @@ time rather than producing a targeted failure. No production behaviour changes i - [ ] [P1-T3] Add two one-line self-closing Compile Include entries to `UtilitiesCS/UtilitiesCS.csproj` for the two new production helpers, adjacent to the existing ArchiveStemContract.cs entry at line 623. The project is legacy non-SDK with an insertion-ordered item list and no globbing, so the entries are mandatory and their placement follows the neighbouring folder entries. Acceptance: the project file contains exactly two new Compile Include lines and the two named files compile. -- [ ] [P1-T4] In `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`, add the declaration-only seams that Phase 1 tests bind against, changing no existing behaviour. Add `public interface IFolderLabelAbsenceReport` with the single member `bool IsAbsentLabel(string folderPath);` and an XML doc recording D4's reason for a separate interface rather than a fourth member on IFolderHierarchyProvider. Declare that the provider implements it, with `public bool IsAbsentLabel(string folderPath) => throw new NotImplementedException("Issue #799: the absence report body is supplied by [P2-T6].");`. Add the optional second constructor parameter `System.Func? archiveRootAccessor = null`, stored in the internal get-only auto-property `internal System.Func? ArchiveRootAccessor { get; }` (D3), leaving the existing null check on the first parameter exactly as it is. Add `internal System.Action? ErrorSink { get; set; }` with an XML doc naming it the injected diagnostic sink that tests observe instead of attaching a log4net appender, so no test mutates the process-global logger repository. Every one of these three declarations carries the `?` annotation because this file opens with `#nullable enable` at line 1 and the [P3-T4] gate runs `/p:TreatWarningsAsErrors=true`: an unannotated `System.Func … = null` is CS8625 and an unannotated never-initialised `System.Action` property is CS8618, and both are promoted to build errors by that gate. Do not change GetAncestorChainAsync, GetImmediateSubfoldersAsync, ResolveLeafKeyAsync or ResolveByUniqueSuffix in this task. Acceptance: the solution compiles; the constructor declares exactly two parameters, the second optional with a null default; and a case-sensitive search of the file finds the single-line tokens `IFolderLabelAbsenceReport`, `ArchiveRootAccessor` and `ErrorSink`; and the analyzer build and the nullable build both exit 0 with the declarations in place, which is the observation that proves no CS8625 or CS8618 was introduced. +- [ ] [P1-T4] In `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`, add the declaration-only seams that Phase 1 tests bind against, changing no existing behaviour. Add `public interface IFolderLabelAbsenceReport` with the single member `bool IsAbsentLabel(string folderPath);` and an XML doc recording D4's reason for a separate interface rather than a fourth member on IFolderHierarchyProvider. Declare that the provider implements it, with `public bool IsAbsentLabel(string folderPath) => throw new NotImplementedException("Issue #799: the absence report body is supplied by [P2-T6].");`. Add the optional second constructor parameter `System.Func? archiveRootAccessor = null`, stored in the internal get-only auto-property `internal System.Func? ArchiveRootAccessor { get; }` (D3), leaving the existing null check on the first parameter exactly as it is. Add `internal System.Action? ErrorSink { get; set; }` with an XML doc naming it the injected diagnostic sink that tests observe instead of attaching a log4net appender, so no test mutates the process-global logger repository. Every one of these three declarations carries the `?` annotation because this file opens with `#nullable enable` at line 1 and the [P3-T4] gate runs `/p:TreatWarningsAsErrors=true`: an unannotated `System.Func … = null` is CS8625 and an unannotated never-initialised `System.Action` property is CS8618, and both are promoted to build errors by that gate. Do not change GetAncestorChainAsync, GetImmediateSubfoldersAsync, ResolveLeafKeyAsync or ResolveByUniqueSuffix in this task. Acceptance: the solution compiles; the constructor declares exactly two parameters, the second optional with a null default; and a case-sensitive search of the file finds the single-line tokens `IFolderLabelAbsenceReport`, `ArchiveRootAccessor` and `ErrorSink`; and no CS8625 or CS8618 diagnostic is introduced by these declarations, proved by the [P1-T5] artifact recording `CS8625-COUNT: 0` and `CS8618-COUNT: 0` and re-proved under enforcement at [P3-T4]. The two gate builds are NOT run in this task: R10 reserves `/t:Rebuild` with the gate switches for [P3-T3] and [P3-T4], and a Phase 1 invocation of either would be an unrecorded command named by no task and writing no artifact. -- [ ] [P1-T5] Build the solution so the seam declarations are available to the test projects, and record `/evidence/regression-testing/p1-t5-seam-build.md`. Acceptance: `EXIT_CODE: 0`, which also proves D2's claim that the optional parameter leaves all 19 existing provider constructions compiling unchanged, including the single-null-argument construction at UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs:316. +- [ ] [P1-T5] Build the solution so the seam declarations are available to the test projects, and record `/evidence/regression-testing/p1-t5-seam-build.md`. Acceptance: `EXIT_CODE: 0`, which also proves D2's claim that the optional parameter leaves all 19 existing provider constructions compiling unchanged, including the single-null-argument construction at UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs:316; and the artifact records the two derived lines `CS8625-COUNT:` and `CS8618-COUNT:`, each the number of occurrences of that diagnostic code in the captured MSBuild output, both of which must be `0`. Nullable diagnostics are emitted as warnings by this build even without `/p:TreatWarningsAsErrors=true`, because the three edited files carry `#nullable enable`, so counting them here is a real observation rather than an inference from the exit code. ```powershell msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" @@ -567,7 +567,7 @@ msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" - [ ] [P1-T10] Add four one-line self-closing Compile Include entries to `UtilitiesCS.Test/UtilitiesCS.Test.csproj` for the four new test files, appended adjacent to the existing folder-test entries at lines 302-304. The project is legacy non-SDK with an insertion-ordered item list and no globbing. Acceptance: the project file contains exactly four new Compile Include lines and the four named files compile. -- [ ] [P1-T11] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` with `[TestClass] public sealed class BreadcrumbBridgeRouterScoreJoinTests` in namespace `QuickFiler.Test.Controllers`, modelled on the strict-mock construction already used at QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs:254-259 (a `Mock`, a `Mock` capturing NavigateToString output, a real `BreadcrumbMessageCodec`, a real `BreadcrumbHtmlRenderer` and a real `BreadcrumbOutboundQueue`). It contains: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` (the AC6 pin: presented row is the archive-relative stem, the score carries the raw rooted path, the bound root is non-empty; assert the rendered document contains the percentage cell for that row); `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage` (D7's additive requirement: the rooted-presented case must not regress); `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` (the public three-argument overload forwards an empty root, so the projection is the identity and no existing caller changes behaviour); `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` (the AC3 pin, asserted as its own test and not as an incidental consequence: with an ancestor chain that begins below the archive root, the bound row's filing target and the joined score key are both still the archive-relative stem); and `BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly` (the spec's integration scenario, driven entirely through the router with no WebView2 and no Outlook: a banner row, a suggestion row, a search-result row, the trash pseudo-row and one stale label; assert lineage on both folder row kinds, the existing fallback on the stale label, and no lineage on the banner or trash rows); `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned` (the AC7 row half, and the only test in this plan that executes the [P2-T12] branch: the provider mock is extended with `.As()` so the router's `provider as IFolderLabelAbsenceReport` cast succeeds, `IsAbsentLabel` is set up to return true for exactly one suggestion row's hierarchy path and false for the others, and `FetchChainAsync` is driven to a null chain for that row; assert the rendered document contains no row for the suppressed label, that every surviving row still carries its own segment keys, and that the surviving row count is one lower than the presented row count); and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed` (decision D-B's zero-candidate restriction at the router boundary: same construction, but `IsAbsentLabel` returns false for the null-chain row, and the row must still be rendered with the existing fallback). Extending the mock with `.As()` is confined to this file and reaches no existing test: no other router test in QuickFiler.Test uses `.As<>()`, so in every one of them the cast still yields null and suppression stays inert exactly as D4 records. Author this file for C# 7.3. QuickFiler.Test.csproj declares no `` and targets v4.8.1 at its line 18, so it compiles at the 7.3 default while every other project in this plan's scope is at Latest, preview or 12.0. Use classic `using (...) { }` blocks, `!= null` rather than `is not null`, explicitly typed `new` rather than target-typed `new`, `switch` statements rather than switch expressions, and no nullable reference annotation anywhere in the file. Mirroring a construct from an existing UtilitiesCS.Test file into this one is the specific failure mode: it surfaces as CS8370 at the [P1-T15] build, not at edit time. Acceptance: the file compiles once [P1-T12] wires it, contains exactly seven `[TestMethod]` attributes, and is at or below 500 lines. +- [ ] [P1-T11] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` with `[TestClass] public sealed class BreadcrumbBridgeRouterScoreJoinTests` in namespace `QuickFiler.Test.Controllers`, modelled on the strict-mock construction already used at QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs:254-259 (a `Mock`, a `Mock` capturing NavigateToString output, a real `BreadcrumbMessageCodec`, a real `BreadcrumbHtmlRenderer` and a real `BreadcrumbOutboundQueue`). It contains: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` (the AC6 pin: presented row is the archive-relative stem, the score carries the raw rooted path, the bound root is non-empty; assert the rendered document contains the percentage cell for that row); `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage` (D7's additive requirement: the rooted-presented case must not regress); `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` (the public three-argument overload forwards an empty root, so the projection is the identity and no existing caller changes behaviour); `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` (the AC3 pin, asserted as its own test and not as an incidental consequence: with an ancestor chain that begins below the archive root, the bound row's filing target and the joined score key are both still the archive-relative stem); and `BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly` (the spec's integration scenario, driven entirely through the router with no WebView2 and no Outlook: a banner row, a suggestion row, a search-result row, the trash pseudo-row and one stale label; assert lineage on both folder row kinds, the existing fallback on the stale label, and no lineage on the banner or trash rows); `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned` (the AC7 row half, and the only test in this plan that takes the TRUE arm of the [P2-T12] suppression predicate: the provider mock is extended with `.As()` so the router's `provider as IFolderLabelAbsenceReport` cast succeeds, `IsAbsentLabel` is set up to return true for exactly one suggestion row's hierarchy path and false for the others, and `FetchChainAsync` is driven to a null chain for that row; assert the rendered document contains no row for the suppressed label, that every surviving row still carries its own segment keys, and that the surviving row count is one lower than the presented row count); and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed` (decision D-B's zero-candidate restriction at the router boundary: same construction, but `IsAbsentLabel` returns false for the null-chain row, and the row must still be rendered with the existing fallback). Extending the mock with `.As()` is confined to this file and reaches no existing test: no other router test in QuickFiler.Test uses `.As<>()`, so in every one of them the cast still yields null and suppression stays inert exactly as D4 records. Call `.As()` on the mock BEFORE that mock's `.Object` is first materialized, because Moq throws at run time when an interface is added to a mock whose object has already been created, and stating the ordering here is what keeps that failure from surfacing for the first time as a red test at [P1-T17]. Author this file for C# 7.3. QuickFiler.Test.csproj declares no `` and targets v4.8.1 at its line 18, so it compiles at the 7.3 default while every other project in this plan's scope is at Latest, preview or 12.0. Use classic `using (...) { }` blocks, `!= null` rather than `is not null`, explicitly typed `new` rather than target-typed `new`, `switch` statements rather than switch expressions, and no nullable reference annotation anywhere in the file. Mirroring a construct from an existing UtilitiesCS.Test file into this one is the specific failure mode: it surfaces as CS8370 at the [P1-T15] build, not at edit time. Acceptance: the file compiles once [P1-T12] wires it, contains exactly seven `[TestMethod]` attributes, and is at or below 500 lines. - [ ] [P1-T12] Add one one-line self-closing Compile Include entry to `QuickFiler.Test/QuickFiler.Test.csproj` for the new test file, appended adjacent to the existing BreadcrumbBridgeRouterIssue439Tests.cs entry at line 64. Acceptance: the project file contains exactly one new Compile Include line and the named file compiles. @@ -607,7 +607,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf - [ ] [P2-T2] Replace the [P1-T2] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` with the real implementation: scan the chain for the first index whose segment FolderPath satisfies `ArchiveStemContract.TryMakeArchiveRelative(segment.FolderPath, archiveRoot, out var stem)` returning true with `stem.Length == 0`, which is exactly the equality case; assign the remainder after that index to `trimmed` and return true; return false with an empty `trimmed` when no such index exists, when the chain is null or empty, and when that index is the last element. A null `chain` or a null `archiveRoot` returns false through an explicit leading guard, and a whitespace-only `archiveRoot` returns false through the contract's own guard at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:113. The null half of that guard is mandatory rather than defensive: the contract declares its two input parameters as non-nullable `string` at ArchiveStemContract.cs:107-108 inside a file that opens with `#nullable enable` at line 1, so passing the [P1-T2] `string? archiveRoot` into it without first narrowing is CS8604, which the [P3-T4] gate promotes to a build error. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the chain trim body is supplied by`. -- [ ] [P2-T3] Relocate the breadcrumb pipeline helper OUT of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` and INTO the new file `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task runs BEFORE [P2-T14] adds the constructor argument; the reverse order puts the viewer-setup file at 501 lines in an intermediate state (R9). Move lines 132-163 verbatim — the two-part comment at 132-136, the `[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]` attribute at 137, and the member body at 138-163 — together with the blank line that separated it from its neighbour. The new file declares `internal partial class QfcItemController` in namespace `QuickFiler.Controllers` and carries only the using directives the moved code needs, which are the ones for the concrete viewer type and for the arrow-event handler; the provider is referenced by its fully qualified name in the moved code and needs no using. This is a pure relocation: no statement is added, removed or reordered. Acceptance: a case-sensitive search of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` returns zero matches for the single-line token `EnsureBreadcrumbPipeline` and that file's line count is at least 30 lines lower than the [P0-T14] baseline of 500; a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` returns one match for that token and one for the single-line token `ExcludeFromCodeCoverage`. +- [ ] [P2-T3] Relocate the breadcrumb pipeline helper OUT of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` and INTO the new file `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task runs BEFORE [P2-T14] adds the constructor argument; the reverse order puts the viewer-setup file at 501 lines in an intermediate state (R9). Move lines 132-163 verbatim — the two-part comment at 132-136, the `[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]` attribute at 137, and the member body at 138-163 — together with the blank line that separated it from its neighbour. The new file declares `internal partial class QfcItemController` in namespace `QuickFiler.Controllers` and carries only the using directives the moved code needs, which are the ones for the concrete viewer type and for the arrow-event handler; the provider is referenced by its fully qualified name in the moved code and needs no using. This is a pure relocation: no statement is added, removed or reordered. The invocation at that file's line 112 is NOT moved and NOT removed; it is an ordinary call from a sibling method of the same partial class and resolves across the partial once [P2-T4] adds the new file to the build. Acceptance: a case-sensitive search of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` returns zero matches for the single-line token `internal void EnsureBreadcrumbPipeline` and exactly one match for the single-line token `EnsureBreadcrumbPipeline();`, which is the surviving call at line 112; that file's line count is at least 30 lines lower than the [P0-T14] baseline of 500; and a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` returns exactly one match for the single-line token `internal void EnsureBreadcrumbPipeline` and one for the single-line token `ExcludeFromCodeCoverage`. - [ ] [P2-T4] Add one one-line self-closing Compile Include entry to `QuickFiler/QuickFiler.csproj` for the new item-controller partial, adjacent to the existing QfcItemController.ViewerSetup.cs entry at line 335. Acceptance: the project file contains exactly one new Compile Include line and the solution compiles, which is the only proof that the relocated member is still in the build. @@ -617,15 +617,15 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf - [ ] [P2-T7] Replace the ProjectSuggestionPath body at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 848-861 with a delegation to `ArchiveStemProjection.ToDisplayStem`, passing the archive root read defensively from the globals so the existing `_globals is null` identity guard is preserved without a separate early return. This eliminates the empty-root one-separator strip that AC4 names, because the contract returns false for a whitespace-only root at ArchiveStemContract.cs:113. Write the delegation as `ArchiveStemProjection.ToDisplayStem(folderPath, root)!`, with the null-forgiving operator and a one-line comment giving its reason: this file opens with `#nullable enable` at line 1 and ProjectSuggestionPath declares a non-nullable `string` return and a non-nullable `string folderPath` parameter at UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:848, while [P1-T1] declares `ToDisplayStem` with a `string?` return; `ToDisplayStem` returns null only when its own `folderPath` argument is null, which this call site's non-nullable parameter excludes, so the operator is sound and it is required because the unsuppressed form is CS8603 and the [P3-T4] gate promotes it to a build error. Do not change ProjectSuggestionPath's signature: it is called from AddSuggestions at line 810 and AddSuggestionRows at line 842 and widening its return would propagate CS8600 into both. Both call sites are display paths and are unchanged: AddSuggestions at line 810 and AddSuggestionRows at line 842. Acceptance: `Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths` and `Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs both still pass, and `AddSuggestions_WhenSuggestionsExist_AppendsHeaderAndTopSuggestions` still passes. -- [ ] [P2-T8] Project each recent entry through the same helper at both AC5 sites in `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`: the string append at line 793 inside AddRecents, and the row-model mirror at line 879 inside AddRecentRows. Both must be projected, because the XML doc at lines 233-242 asserts that the string list and the row list are text-identical and the row list is the one the breadcrumb surfaces actually consume, so projecting only one would break a documented contract. Acceptance: all four `[TestMethod]` tests in `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` pass, and `AddRecents_WhenRecentsExist_AppendsHeaderAndEntries`, `FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents` and `FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds` all still pass, because their fixtures use already-relative recents for which the projection is the identity. +- [ ] [P2-T8] Project each recent entry through the same helper at both AC5 sites in `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`: the string append at line 793 inside AddRecents, and the row-model mirror at line 879 inside AddRecentRows. Both must be projected, because the XML doc at lines 233-242 asserts that the string list and the row list are text-identical and the row list is the one the breadcrumb surfaces actually consume, so projecting only one would break a documented contract. Write both projections as `ArchiveStemProjection.ToDisplayStem(recent, root)!`, with the null-forgiving operator and a one-line comment giving its reason, for the same reason [P2-T7] carries one. This file opens with `#nullable enable` at line 1 and [P1-T1] declares `ToDisplayStem` with a `string?` return, while both consuming positions are non-nullable: `List.AddRange` at the string-append site takes an `IEnumerable`, so an unsuppressed `Select` over the projection is CS8620, and the `FolderRow` constructor at UtilitiesCS/OutlookObjects/Folder/FolderRow.cs:42 declares a non-nullable `string text` first parameter, so an unsuppressed argument at the row site is CS8604; the [P3-T4] gate promotes either to a build error. The operator is sound rather than defensive because `ToDisplayStem` returns null only when its own `folderPath` argument is null, and every entry of `_globals.AF.RecentsList` is a non-nullable `string` — the member is declared `SloLinkedList RecentsList { get; }` at UtilitiesCS/Interfaces/IGlobals/IAppAutoFileObjects.cs:22. The two comment lines are inside the D11 budget: FolderPredictor.cs has at least eight lines of derived slack against its 1003-line no-growth budget. Acceptance: all four `[TestMethod]` tests in `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` pass, and `AddRecents_WhenRecentsExist_AppendsHeaderAndEntries`, `FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents` and `FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds` all still pass, because their fixtures use already-relative recents for which the projection is the identity. -- [ ] [P2-T9] Rewrite the include-children TRUE branch of GetOlSubpath at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 955-965 as a verified prefix removal through `ArchiveStemContract.TryMakeArchiveRelative`, falling back to the input path when the contract returns false, so a path that does not start with the ancestor yields a diagnosable value instead of a garbage substring and a path no longer than the ancestor no longer throws ArgumentOutOfRangeException. The contract's parameter is only NAMED archiveRoot and is root-agnostic, which is what lets it serve this site, where the ancestor is a search root supplied by the caller. Leave the include-children FALSE branch at lines 966-970 exactly as it is: it computes a leaf name, which the contract does not do, and converting it would change a different function. Acceptance: `GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs still passes, and both GetRelevantOlPathPortion tests in ToDoModel.Test, directory Email Utilities, file FolderHandlerTests_Written.cs still pass. +- [ ] [P2-T9] Rewrite the include-children TRUE branch of GetOlSubpath at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 955-965 as a verified prefix removal through `ArchiveStemContract.TryMakeArchiveRelative`, falling back to the input path when the contract returns false, so a path that does not start with the ancestor yields a diagnosable value instead of a garbage substring and a path no longer than the ancestor no longer throws ArgumentOutOfRangeException. The contract's parameter is only NAMED archiveRoot and is root-agnostic, which is what lets it serve this site, where the ancestor is a search root supplied by the caller. Leave the include-children FALSE branch at lines 966-970 exactly as it is: it computes a leaf name, which the contract does not do, and converting it would change a different function. Acceptance: `GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs still passes, and both GetRelevantOlPathPortion tests in ToDoModel.Test, directory Email Utilities, file FolderHandlerTests_Written.cs still pass; those two ToDoModel.Test assertions are not executed by any Phase 2 command in this plan, and the earliest scheduled run that includes that assembly is [P3-T5], whose `NEWLY-FAILING: NONE` condition is where a regression in them surfaces. - [ ] [P2-T10] Replace the ProjectPredeterminedFolder body at `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` lines 272-285 with a one-line delegation to `ArchiveStemProjection.ToDisplayStem`, retaining the member itself so its existing test keeps a target, and rewrite the XML doc at lines 252-271 so the two paragraphs describing the empty-root divergence are replaced by a statement that both members now share one projection and that the empty-root strip was removed by AC4. Also rewrite the duplication-rationale comment at lines 223-230, whose stated reason — that FolderPredictor.ProjectSuggestionPath is private and lives under UtilitiesCS, which the earlier change could not modify — becomes false the moment the shared helper is public. Acceptance: `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` passes as retargeted by [P1-T14]; a case-sensitive search of the file returns zero matches for the single-line literal `is private and lives under UtilitiesCS`; and the file is at or below its [P0-T14] baseline of 312 lines. - [ ] [P2-T11] Project the score paths inside the internal four-argument BindRowsAsync at `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` lines 92-150, immediately before the `_builder.BuildRows` call at lines 132-136, using the bound root the method already normalizes at lines 107-109. Build a new score list that contains every original score UNCHANGED and, additionally, one projected `FolderScore` carrying `ArchiveStemProjection.ToDisplayStem(score.FolderPath, _boundRoot)!` with the same score and probability whenever that projection differs from the original path under an ordinal comparison. The null-forgiving operator carries a one-line comment giving its reason: this file opens with `#nullable enable` at line 1, [P1-T1] declares `ToDisplayStem` with a `string?` return, and `ToDisplayStem` returns null only when its own `folderPath` argument is null, which the projection loop excludes by skipping any score whose `FolderPath` is null before calling; the unsuppressed form is CS8600 or CS8604 at the `FolderScore` construction and the [P3-T4] gate promotes either to a build error. The addition is what makes it safe: substitution would fix the stem-presented case and silently break the rooted-presented case (D7), and the probability index at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:224 assigns through the indexer, so duplicate keys are tolerated rather than throwing. The public three-argument overload at lines 75-82 forwards an empty root, so the projection is the identity for every caller of that overload and no existing behaviour changes there. Do not modify the row builder and do not change any public signature; re-keying the join was considered and rejected because BuildRows takes only a string list and a score sequence, so there is no correlating identity to key on without changing a public signature and every test that calls it. Acceptance: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage`, `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` and `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` all pass, and `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` and `Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` in QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs both still pass. -- [ ] [P2-T12] Apply the AC7 zero-candidate row suppression on the Efc surface only, per the escalation branch D5 records, inside the same internal BindRowsAsync in `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`. Add a private readonly `IFolderLabelAbsenceReport` field assigned in the existing constructor body at lines 48-56 from `provider as IFolderLabelAbsenceReport`, which changes no constructor signature and therefore breaks no existing test; a `Mock` is not an `IFolderLabelAbsenceReport`, so the field is null and suppression is inert in every existing router test (D4). In the chain loop at lines 110-130, when a suggestion row's hierarchy path is non-null, its fetched chain is null, and the absence report says that hierarchy path is an absent label, record the presented text in a suppression set; a null chain arising from cancellation or from a provider fault is NOT suppressed, because those rows are not known-absent. Derive the retained presented-row list from the suppression set and pass that SAME list to both `_builder.BuildRows` at lines 132-136 and `AttachSegmentKeys` at line 137, because AttachSegmentKeys indexes the presented rows by row index at line 176 and would mis-align against an unfiltered list. Log the suppressed count at DEBUG through the `log` field declared at lines 21-23. When nothing is suppressed, pass the original list unchanged so the common path allocates nothing. Acceptance: the solution compiles; every `[TestMethod]` in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` passes, including `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned`, which is the only test that executes this task's branch, and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed`, which pins the zero-candidate restriction at the router boundary; and all ten tests of the partial class `BreadcrumbBridgeRouterIssue439Tests`, across both its files, still pass, which is the observable proof that suppression is inert behind a provider mock that does not implement the absence report. +- [ ] [P2-T12] Apply the AC7 zero-candidate row suppression on the Efc surface only, per the escalation branch D5 records, inside the same internal BindRowsAsync in `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`. Add a private readonly `IFolderLabelAbsenceReport` field assigned in the existing constructor body at lines 48-56 from `provider as IFolderLabelAbsenceReport`, which changes no constructor signature and therefore breaks no existing test; a `Mock` is not an `IFolderLabelAbsenceReport`, so the field is null and suppression is inert in every existing router test (D4). In the chain loop at lines 110-130, when a suggestion row's hierarchy path is non-null, its fetched chain is null, and the absence report says that hierarchy path is an absent label, record the presented text in a suppression set; a null chain arising from cancellation or from a provider fault is NOT suppressed, because those rows are not known-absent. Derive the retained presented-row list from the suppression set and pass that SAME list to both `_builder.BuildRows` at lines 132-136 and `AttachSegmentKeys` at line 137, because AttachSegmentKeys indexes the presented rows by row index at line 176 and would mis-align against an unfiltered list. Log the suppressed count at DEBUG through the `log` field declared at lines 21-23. When nothing is suppressed, pass the original list unchanged so the common path allocates nothing. Acceptance: the solution compiles; every `[TestMethod]` in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` passes, including `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned`, which takes the true arm of this task's suppression predicate, and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed`, which takes its false arm and pins the zero-candidate restriction at the router boundary; and all ten tests of the partial class `BreadcrumbBridgeRouterIssue439Tests`, across both its files, still pass, which is the observable proof that suppression is inert behind a provider mock that does not implement the absence report. - [ ] [P2-T13] Add the lazy root accessor argument to the provider construction at `QuickFiler/Controllers/EfcFormController.cs` lines 1053-1055, passing a delegate that reads the archive root from the existing application-globals accessor at call time rather than at construction time. The delegate form is mandatory: the archive-root property throws when the root is unresolvable, this construction is not inside a try, and QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242 exists because of that throw (D2). Change nothing else in this file; the raw score read at lines 1115-1117 and the four-argument router call at line 1118 stay exactly as they are, because AC6 is delivered in the router (D7) and this file cannot absorb growth. Acceptance: the solution compiles; `BindBreadcrumbRowsAsync_WhenArchiveRootThrows_ReportsOnceAndDoesNotThrow` still passes; and the file is at or below 1322 lines, that is its [P0-T14] baseline of 1320 plus the at-most-two lines the added argument costs (D11). @@ -808,9 +808,16 @@ git grep -n -E 'Replace\(["'']_|letter-spacing|text-transform|first-letter|word- - [ ] [P3-T18] Check off AC6 in `spec.md` line 839, citing [P1-T17] and [P2-T16] and the passing `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, and recording the two deviations from the spec's own wording: the projection is applied in the Efc router rather than at the controller call site named by the criterion, because the controller is 1320 lines and cannot absorb growth while the router already normalizes the bound root; and the projected score is ADDED alongside the raw score rather than substituted for it, per D7. Acceptance: exactly one AC checkbox changes in this task and the AC6 line carries `- [x]`. -- [ ] [P3-T19] Check off AC7 in `spec.md` line 840, citing the passing `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` from [P2-T16] for the logging half, and the passing `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned` and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed` from [P2-T16] for the row-suppression half, which are the only two tests in this plan that execute the [P2-T12] branch, and recording three things: that the filtered branch is taken restricted to the zero-candidate case per decision D-B; that "per session" is realized as "per provider instance" and is enforced by a thread-safe per-instance set rather than a static one; and that the escalation branch D5 records was taken, so row suppression is delivered on the Efc surface only while the logging half is delivered on both surfaces. Acceptance: exactly one AC checkbox changes in this task, the AC7 line carries `- [x]`, and the check-off note names the escalation deviation. +- [ ] [P3-T19] Check off AC7 in `spec.md` line 840, citing the passing `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` from [P2-T16] for the logging half, and the passing `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned` and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed` from [P2-T16] for the row-suppression half, which are the only two tests in this plan that reach the [P2-T12] suppression predicate, the first on its true arm and the second on its false arm, and recording three things: that the filtered branch is taken restricted to the zero-candidate case per decision D-B; that "per session" is realized as "per provider instance" and is enforced by a thread-safe per-instance set rather than a static one; and that the escalation branch D5 records was taken, so row suppression is delivered on the Efc surface only while the logging half is delivered on both surfaces. Acceptance: exactly one AC checkbox changes in this task, the AC7 line carries `- [x]`, and the check-off note names the escalation deviation. -- [ ] [P3-T20] Check off AC8 in `spec.md` line 841, citing `/evidence/qa-gates/p3-t12-ac8-verification.md` and recording that AC8 is satisfied by the verified finding that the renderer does not alter a leading underscore, that no code change was made, and that a renderer change would have been a defect. Acceptance: exactly one AC checkbox changes in this task, the AC8 line carries `- [x]`, and no file under QuickFiler/Resources appears in the [P3-T11] scope enumeration. +- [ ] [P3-T20] Check off AC8 in `spec.md` line 841, citing `/evidence/qa-gates/p3-t12-ac8-verification.md` and recording that AC8 is satisfied by the verified finding that the renderer does not alter a leading underscore, that no code change was made, and that a renderer change would have been a defect. Acceptance: exactly one AC checkbox changes in this task; the AC8 line carries `- [x]`; and `/evidence/qa-gates/p3-t20-ac8-assets.md` records the literal field lines `Timestamp:`, `Command:`, `EXIT_CODE:` and `Output Summary:` together with the derived line `AC8-ASSET-PATHS-CHANGED: 0`, derived from the anchored diff and the porcelain companion below. That check is made here rather than read off [P3-T11], because R7 scopes the [P3-T11] enumeration to the source pathspec `'*.cs' '*.csproj'` and the QuickFiler resources directory contains no `.cs` and no `.csproj` file, so an assertion that it is absent from that enumeration is true for every possible execution and gates nothing. + +```powershell +$BaseSha = (Select-String -Path 'docs\features\active\2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799\evidence\baseline\p0-t2-base.md' -CaseSensitive -Pattern '^BASE-SHA: ([0-9a-f]{40})$').Matches[0].Groups[1].Value +git add --intent-to-add -- 'QuickFiler/Resources' +git diff --name-only $BaseSha -- 'QuickFiler/Resources' +git status --porcelain --untracked-files=all -- 'QuickFiler/Resources' +``` - [ ] [P3-T21] Update the `spec.md` Status line to `Implemented` and add an "Outcome" note under Rollout & Follow-up recording the four deviations this plan makes from the spec's own prose, each with its reason: AC7 row suppression is delivered on the Efc surface only, because the QuickFiler presented row set is composed solely inside the sibling-owned bridge router, which decision D-B forbids this item from editing (D5); the AC6 score projection is additive rather than substitutive, so the rooted-presented-text join cannot regress (D7); the two #439 Efc router test files listed in the Write Set carry no hunk, because every test in both drives a mocked provider below the trim boundary and editing their shared chain fixture would break unrelated #614 boundary tests (D8); and the AC7 absence classification is published through a new small public interface declared in the provider's own file rather than through a fourth member on the shared hierarchy contract, which net48 cannot add without breaking every implementer and every strict mock (D4). Acceptance: the Status line reads `Implemented` and all four deviations are recorded by name. @@ -867,6 +874,44 @@ was re-derived directly against this worktree in this pass, together with the si - Record-hygiene correction found by re-reading the bounded record itself in this pass: the round 1 record carried a CITATION line whose path token contained a space (the ToDoModel email-utilities test file). That contradicted this plan's own R12 and D10 convention, which requires both space-containing paths to be named in words only, and it is not a well-formed path token. The line was removed; the fact it carried is retained in D9, which names that file in words and gives its two GetOlSubpath assertion ranges. - Sibling consequence of the [P1-T11] test-count change, re-checked in this pass: the task's former closing claim that "every provider interaction is Moq-supplied, so the AC7 suppression path is inert in this file by construction" is no longer true of this file and was replaced by the narrower claim the new tests actually support, that no OTHER router test is reached. [P2-T12]'s acceptance and [P3-T19]'s check-off citation were both updated to name the two new tests, and the AC7 row of AC-MAPPING now carries them. +Round 3 (revision pass against the orchestrator-adjudicated round 2 preflight delta). Every citation this pass's edits +touch was re-derived directly against this worktree in this pass, together with the sibling lines in the same region: + +- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs. The bare invocation `EnsureBreadcrumbPipeline();` stands at line + 112, inside the WebView2 initialization method whose body closes at line 130, and the declaration + `internal void EnsureBreadcrumbPipeline()` stands at line 138 under the two-part comment at 132-136 and the + ExcludeFromCodeCoverage attribute at 137, with the member body closing at 163. The relocation moves 132-163 only, so the + call at 112 survives it; the former acceptance demanded zero matches for the bare identifier and could therefore never + pass. Sibling re-check: the next member's XML doc begins at line 165 and its signature at 166, so removing 132-163 plus + the blank separator at 164 leaves no orphaned comment and no orphaned attribute. +- UtilitiesCS/OutlookObjects/Folder/FolderRow.cs. `#nullable enable` at line 1; the constructor + `public FolderRow(string text, FolderRowKind kind, FolderScore? score)` at line 42 with a NON-nullable first parameter; + `public string Text { get; }` at line 50 and `public FolderScore? Score { get; }` at line 60. This is the fifth consumer + of the nullable projection return and it is the one the round 2 pass missed while narrowing [P2-T7] and [P2-T11]. +- UtilitiesCS/Interfaces/IGlobals/IAppAutoFileObjects.cs:22. `SloLinkedList RecentsList { get; }`, a non-nullable + element type, with a commented-out predecessor declaration on line 21. This is what makes the [P2-T8] null-forgiving + operator sound rather than defensive. +- UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs, both AC5 sites and their siblings. AddRecents at 788-795 appends + through `folderList.AddRange(_globals.AF.RecentsList)` at line 793, so the projected form is an `IEnumerable` + argument; AddRecentRows at 866-882 constructs `new FolderRow(recent, FolderRowKind.Recent, null)` at line 879, so the + projected form is a non-nullable first argument. Sibling re-check in the same file: + `public string GetOlSubpath(string path, string olAncestor, bool includeChildren)` is declared at line 953 with both + string parameters and the return NON-nullable, so [P2-T9], which routes through the strict contract rather than through + the lenient projection, needs no narrowing and none was added to it. +- QuickFiler/Controllers/QfcItemController.FolderHandling.cs. Zero `#nullable` directives anywhere in the file, re-confirmed + in this pass, so [P2-T10]'s delegation remains the one consumer that needs no suppression. +- QuickFiler/Resources. Forty-three files, of which none is a `.cs` file and none is a `.csproj` file; the only markup + assets are two HTML files and the remainder are images and design sources. This is the premise that made the former + [P3-T20] third conjunct true for every possible execution under the R7 source pathspec, and the reason the replacement + conjunct measures that directory directly. +- Artifact-name check for the two command blocks this pass touches: [P0-T2] writes p0-t2-base.md, which is the exact + filename the new [P3-T20] `$BaseSha` binding reads and the same one R11 and [P3-T1], [P3-T7] and [P3-T11] already read. +- Consumer sweep, run to closure in this pass so no sixth consumer is left unnarrowed: the lenient projection's return is + read at exactly five call sites across four tasks — ProjectSuggestionPath in [P2-T7], the string append and the row mirror + in [P2-T8], ProjectPredeterminedFolder in [P2-T10] and the score projection in [P2-T11]. Three of the five are inside a + `#nullable enable` file and now each carry a reasoned null-forgiving operator; [P2-T10]'s file has no nullable context; + and [P2-T9] is not a consumer of the projection at all. + Delta self-check: this document's own prose was checked against the rules it enforces. Every backticked path with a directory separator is a Write Set path taken from spec.md lines 645-702; every sibling-owned, precedent, comparison and out-of-scope path is written in bare prose; both space-containing paths are named in words only and neither is in this @@ -921,10 +966,7 @@ AC-MAPPING: AC4 | IMPLEMENTATION: P2-T1, P2-T7, P2-T9, P2-T10 | TESTS: P1-T6, P1 AC-MAPPING: AC5 | IMPLEMENTATION: P2-T8 | TESTS: P1-T9 | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p2-t16-pass-after.md AC-MAPPING: AC6 | IMPLEMENTATION: P2-T11 | TESTS: P1-T11 | EVIDENCE: /evidence/regression-testing/p1-t17-qft-fail.md and /evidence/regression-testing/p2-t16-pass-after.md AC-MAPPING: AC7 | IMPLEMENTATION: P2-T6, P2-T12 | TESTS: P1-T8 for the logging half and P1-T11 for the row-suppression half, specifically BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned and BindRowsAsync_AmbiguousLabel_IsNotSuppressed | EVIDENCE: /evidence/regression-testing/p1-t16-ut-fail.md and /evidence/regression-testing/p1-t17-qft-fail.md and /evidence/regression-testing/p2-t16-pass-after.md -AC-MAPPING: AC8 | IMPLEMENTATION: P3-T12 | TESTS: P3-T12 | EVIDENCE: /evidence/qa-gates/p3-t12-ac8-verification.md +AC-MAPPING: AC8 | IMPLEMENTATION: P3-T12 | TESTS: P3-T12 | EVIDENCE: /evidence/qa-gates/p3-t12-ac8-verification.md and /evidence/qa-gates/p3-t20-ac8-assets.md UNRESOLVED-GAPS: NONE -DIRECTIVE: PREFLIGHT VALIDATION ONLY -DIRECTIVE NOTE: the line below is the mechanical handoff signal. It is not a discovered defect in this revision; clearance is outstanding because validation-only preflight has NOT been run by this planner, no atomic-executor delegation tool and no MCP plan validator being present in this planner's tool surface. -PREFLIGHT: REVISIONS REQUIRED -The orchestrator must obtain a genuine `PREFLIGHT: ALL CLEAR` from atomic-executor under `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, and a passing `mcp__drm-copilot__validate_orchestration_artifacts` run with `artifact_type: "plan"`, before execution begins. This plan is not self-approved. -CONVERGENCE: NO FURTHER ROUNDS EXPECTED — every accepted item of the round 1 delta is applied, the two narrowed items are applied in their narrowed form, and the one refuted item is left unapplied as instructed. The three judgment calls the previous round flagged (the D5 escalation to Efc-only row suppression, the D8 no-hunk finding on the two #439 Efc router test files, and the D7 additive score projection) were each re-checked by the reviewer against the tree in round 1 and confirmed, so they are no longer open. The only new material introduced in this pass is the nullable-narrowing consequence of the accepted annotation items, which is closed inside the four tasks that consume the annotated members. + +Clearance for this plan is the atomic-executor preflight return recorded in the orchestrator checkpoint, not a line in this file. From 0c3c21fc8d3c7876c8d35233a9198df5a545f68a Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Mon, 7 Sep 2026 06:52:56 -0400 Subject: [PATCH 08/13] docs(799): capture Phase 0 baseline and bootstrap evidence Executes Phase 0 tasks P0-T1 through P0-T15 of the approved atomic plan. Bootstraps the worktree (repo-local SDK 8.0.205, 172 NuGet packages, CSharpier 1.2.6, dotnet-coverage) and records the base commit, the two MSBuild gate baselines (0 warnings, 0 errors each), the CSharpier check baseline (1593 files, exit 0), the two suite baselines (4786 and 1363 tests, 0 failures), the nine-assembly coverage baseline (7048 tests, 84.55 percent comparability line index), Write Set measurability and line-count baselines, and the pre-change status of the 18 named tests. Co-Authored-By: Claude Fable 5.1 --- .../evidence/baseline/p0-t10-nullable.md | 30 +++++++ .../evidence/baseline/p0-t11-suites.md | 55 ++++++++++++ .../evidence/baseline/p0-t12-coverage.md | 70 +++++++++++++++ .../evidence/baseline/p0-t13-measurability.md | 52 ++++++++++++ .../evidence/baseline/p0-t14-sizes.md | 85 +++++++++++++++++++ .../evidence/baseline/p0-t15-tests.md | 54 ++++++++++++ .../evidence/baseline/p0-t2-base.md | 17 ++++ .../evidence/baseline/p0-t3-sdk.md | 30 +++++++ .../evidence/baseline/p0-t4-restore.md | 54 ++++++++++++ .../evidence/baseline/p0-t5-tools.md | 27 ++++++ .../baseline/p0-t6-dotnet-coverage.md | 28 ++++++ .../evidence/baseline/p0-t7-vstest.md | 27 ++++++ .../evidence/baseline/p0-t8-csharpier.md | 22 +++++ .../evidence/baseline/p0-t9-analyzers.md | 31 +++++++ .../baseline/phase0-instructions-read.md | 26 ++++++ .../plan.2026-09-06T22-01.md | 30 +++---- 16 files changed, 623 insertions(+), 15 deletions(-) create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t10-nullable.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t11-suites.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t12-coverage.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t13-measurability.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t14-sizes.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t15-tests.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t2-base.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t3-sdk.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t4-restore.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t5-tools.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t6-dotnet-coverage.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t7-vstest.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t8-csharpier.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t9-analyzers.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/phase0-instructions-read.md diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t10-nullable.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t10-nullable.md new file mode 100644 index 000000000..4c64c53a3 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t10-nullable.md @@ -0,0 +1,30 @@ +# [P0-T10] Nullable-build baseline + +Timestamp: 2026-09-07T07-00 + +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true + +EXIT_CODE: 0 + +## MSBuild summary + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) +``` + +Elapsed 00:00:16.28. Console output captured at default (normal) verbosity, 11624 lines. An independent scan of +the captured output found 0 lines carrying `: warning `, 0 lines carrying `: error `, and 0 lines carrying the +`CS86` nullable diagnostic prefix, which agrees with the summary counters. + +- WARNINGS: 0 +- ERRORS: 0 + +Output Summary: The nullable gate is green at the base commit. The command is character-for-character the +CLAUDE.md nullable command: `/p:Nullable=enable` was not added, and `/t:Build` was not substituted for +`/t:Rebuild`, so the gate could actually fail rather than exiting 0 with `CoreCompile` skipped. Nullable +enforcement in this repository is per-file opt-in through `#nullable enable`, and no opted-in file in the solution +produced a CS86xx diagnostic at this commit. MSBuild is not on this machine's PATH, so the Visual Studio 18 amd64 +MSBuild directory was prepended to `PATH` in the invoking shell; the command itself is unmodified. Host paths +reduced per R3. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t11-suites.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t11-suites.md new file mode 100644 index 000000000..dbabe9d8a --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t11-suites.md @@ -0,0 +1,55 @@ +# [P0-T11] UtilitiesCS.Test and QuickFiler.Test baseline runs + +Timestamp: 2026-09-07T07-05 + +Command: & $vstest UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p0-t11-ut' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests' +(then) & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p0-t11-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook' + +EXIT_CODE: 0 + +EXIT-CODE-UT: 0 +EXIT-CODE-QFT: 0 + +The single `EXIT_CODE:` field is the larger of the two invocation exit codes, both of which are 0. + +## Derived counters (read from TRX `ResultSummary/Counters`) + +BASELINE-UT-TOTAL: 4786 +BASELINE-UT-PASSED: 4786 +BASELINE-UT-FAILED: 0 +BASELINE-QFT-TOTAL: 1363 +BASELINE-QFT-PASSED: 1363 +BASELINE-QFT-FAILED: 0 + +Each `BASELINE-*-FAILED` value was read from its run's TRX `ResultSummary/Counters` `failed` attribute, not from +the console, because vstest prints no `Failed:` line at all on a fully passing run. + +## TRX documents read + +- UtilitiesCS.Test: `TestResults\799-p0-t11-ut\__2026-09-07_06_44_06_net481.trx` — the only TRX in + that results directory, so no most-recently-modified selection was needed. +- QuickFiler.Test: `TestResults\799-p0-t11-qft\__2026-09-07_06_46_42_net481.trx` — the only TRX in + that results directory (count verified as 1), so no most-recently-modified selection was needed. + +No TRX content is pasted into this artifact (R3); only parsed counter values are recorded. TRX filenames are +reduced per R3: the `runUser` and `computerName` segments in the generated file names are replaced with `` +and ``. + +## Excluded classes (R13) + +The UtilitiesCS.Test run excludes the four shell-icon classes that stall vstest on this machine, through +`FullyQualifiedName!~` clauses: + +1. HelperClasses.ShellUtilities_Tests +2. HelperClasses.ShellUtilitiesStatic_Tests +3. HelperClasses.SysImageListHelperTests +4. EmailIntelligence.OSBrowser_Tests + +Both runs additionally exclude `TestCategory=LiveOutlook`. The QuickFiler.Test run carries no shell-icon clause, +because those classes live in UtilitiesCS.Test. The reduced denominator recorded here is the same denominator the +Phase 2 and Phase 3 comparisons use. + +Output Summary: Both baseline suites are fully green at the base commit. UtilitiesCS.Test ran 4786 tests with 4786 +passed and 0 failed in 32.5 seconds; QuickFiler.Test ran 1363 tests with 1363 passed and 0 failed in 13.4 seconds. +Both invocations exited 0 and both printed `Test Run Successful.`. No pre-existing failure has to be carried into +the Phase 2 no-newly-failing comparison. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t12-coverage.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t12-coverage.md new file mode 100644 index 000000000..8884f352f --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t12-coverage.md @@ -0,0 +1,70 @@ +# [P0-T12] Nine-assembly coverage baseline + +Timestamp: 2026-09-07T07-12 + +Command: dotnet-coverage collect --output coverage\799-baseline.cobertura.xml --output-format cobertura --settings coverage\799-effective-coverage.config -- $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll SVGControl.Test\bin\Debug\SVGControl.Test.dll Tags.Test\bin\Debug\Tags.Test.dll TaskMaster.Test\bin\Debug\TaskMaster.Test.dll TaskTree.Test\bin\Debug\TaskTree.Test.dll TaskVisualization.Test\bin\Debug\TaskVisualization.Test.dll ToDoModel.Test\bin\Debug\ToDoModel.Test.dll UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll VBFunctions.Test\bin\Debug\VBFunctions.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p0-t12' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests' +(then the pinned D13 aggregation block over coverage\799-baseline.cobertura.xml) + +EXIT_CODE: 0 + +## Aggregation output line (verbatim, printed by the pinned block) + +``` +LINES_COVERED=112855 LINES_VALID=133485 BRANCHES_COVERED=26642 BRANCHES_VALID=33624 PACKAGES_MATCHED=9 +``` + +BASELINE-LINES-COVERED: 112855 +BASELINE-LINES-VALID: 133485 +BASELINE-BRANCHES-COVERED: 26642 +BASELINE-BRANCHES-VALID: 33624 +BASELINE-PACKAGES-MATCHED: 9 + +## Derived percentages + +BASELINE-LINE-PERCENT: 84.55 +BASELINE-BRANCH-PERCENT: 79.24 + +BASELINE_FLOOR: MET — measured against the D13 comparability index (84.55 percent lines) and not against the +repository line-coverage rate. The D13 aggregation counts every `line` element under a matched package, which +selects class-level and method-level elements alike and therefore over-counts the denominator relative to a +de-duplicated per-line count. The value is sound for the identical-method comparison [P3-T8] makes and is not a +policy measurement. No task in this plan gates on it, and a pre-existing repository floor result would not halt +the plan either way. + +## Test counters + +BASELINE-TOTAL-TESTS: 7048 +BASELINE-FAILED-TESTS: 0 + +Read from the TRX `ResultSummary/Counters` element: total 7048, passed 7048, failed 0, aborted 0. The run printed +`Test Run Successful.` and `Total tests: 7048 / Passed: 7048`. + +## Packages enumerated + +The Cobertura document contains 14 `package` elements. The nine first-party packages the aggregation matches are +QuickFiler, SVGControl, Tags, TaskMaster, TaskTree, TaskVisualization, ToDoModel, UtilitiesCS and VBFunctions, so +`PACKAGES_MATCHED` is 9 and no package-name mismatch occurred. The five packages present but not matched are +log4net, Mono.Reflection, Microsoft.IO.RecyclableMemoryStream, System.Linq.Async and System.Interactive, all +third-party. + +## TRX document read + +`TestResults\799-p0-t12\__2026-09-07_06_48_12_net481.trx` — the only TRX in that results directory +(count verified as 1), so no most-recently-modified selection was needed. Filename reduced per R3; no TRX content +is pasted. + +## Exclusions (R13, D14) + +The run excludes `TestCategory=LiveOutlook` and the four shell-icon classes HelperClasses.ShellUtilities_Tests, +HelperClasses.ShellUtilitiesStatic_Tests, HelperClasses.SysImageListHelperTests and +EmailIntelligence.OSBrowser_Tests. The nine test assemblies are named explicitly on the command line, so no +worktree copy under a `.claude` segment can be enumerated or loaded. Instrumentation used the derived settings +document `coverage\799-effective-coverage.config`, which is `coverage.config` plus one appended +`` entry excluding `*.Test.dll` modules from the measured denominator. + +Output Summary: The full nine-assembly suite ran green under `dotnet-coverage` (D12 form, not +`/EnableCodeCoverage`): 7048 tests, 7048 passed, 0 failed, exit code 0, 55.5 seconds. The pinned D13 aggregation +matched all nine first-party packages and produced 112855 covered of 133485 valid lines (84.55 percent) and 26642 +covered of 33624 valid branches (79.24 percent). These five counters are the baseline side of the [P3-T8] +comparison, which must apply this identical aggregation to the post-change document and must state `lines-valid` +comparability as an explicit precondition. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t13-measurability.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t13-measurability.md new file mode 100644 index 000000000..400750f67 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t13-measurability.md @@ -0,0 +1,52 @@ +# [P0-T13] Coverage measurability of the Write Set production files + +Timestamp: 2026-09-07T07-14 + +Command: separator-anchored `class` `filename` query over coverage\799-baseline.cobertura.xml +(the [P0-T13] command block, matching `$f.EndsWith('\' + $n) -or $f.EndsWith('/' + $n)`) + +EXIT_CODE: 0 + +## Class-element counts the determination was made from + +``` +OutlookFolderHierarchyProvider.cs classElements=6 +FolderPredictor.cs classElements=13 +QfcItemController.FolderHandling.cs classElements=6 +BreadcrumbBridgeRouter.cs classElements=6 +EfcFormController.cs classElements=43 +QfcItemController.ViewerSetup.cs classElements=9 +ArchiveStemContract.cs classElements=1 +``` + +## Determination + +MEASURABLE: UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs +MEASURABLE: UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs +MEASURABLE: QuickFiler/Controllers/QfcItemController.FolderHandling.cs +MEASURABLE: QuickFiler/Controllers/BreadcrumbBridgeRouter.cs +MEASURABLE: QuickFiler/Controllers/EfcFormController.cs +MEASURABLE: QuickFiler/Controllers/QfcItemController.ViewerSetup.cs +MEASURABLE: UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs + +Seven `MEASURABLE:`/`UNMEASURABLE:` lines are recorded: one for each of the six existing Write Set production +paths, plus ArchiveStemContract.cs. ArchiveStemContract.cs is not modified by this plan; its measurability is +recorded because [P2-T1], [P2-T2] and [P2-T9] all route through it, so a zero-class-element result there would +explain an otherwise puzzling [P3-T7] outcome. It reports one class element, so that explanation does not apply. + +## Files this plan creates (measured for the first time by [P3-T9]) + +NEW: UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs +NEW: UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs + +## Match anchoring + +The trailing-name match is anchored on a directory separator, so an unanchored suffix cannot over-select a sibling +whose name merely ends with the same characters. The concrete case this protects against in this Write Set is +UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs, a sibling-owned file whose name ends with the +characters of BreadcrumbBridgeRouter.cs but is preceded by `r` rather than by a separator, so it is correctly +excluded from the count of 6 recorded for QuickFiler/Controllers/BreadcrumbBridgeRouter.cs. + +Output Summary: All seven queried production files are measurable in the baseline Cobertura document, with class +element counts ranging from 1 to 43 and no zero result. No Write Set production file is invisible to the coverage +harness, so the [P3-T7] changed-line coverage determination has a source of data for every file it must inspect. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t14-sizes.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t14-sizes.md new file mode 100644 index 000000000..fc0def606 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t14-sizes.md @@ -0,0 +1,85 @@ +# [P0-T14] Baseline line counts of the files this plan edits or creates + +Timestamp: 2026-09-07T07-16 + +Command: (Get-Content -LiteralPath ).Count for each path below + +EXIT_CODE: 0 + +CEILING: 500 (applies to *.cs only) + +## Existing Write Set production paths (.cs) + +- UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs = 141 +- UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs = 1003 +- QuickFiler/Controllers/QfcItemController.FolderHandling.cs = 312 +- QuickFiler/Controllers/BreadcrumbBridgeRouter.cs = 304 +- QuickFiler/Controllers/EfcFormController.cs = 1320 +- QuickFiler/Controllers/QfcItemController.ViewerSetup.cs = 500 + +## Retargeted Write Set test paths (.cs) + +- UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs = 479 +- QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs = 354 + +## NO HUNK test paths from D8 (.cs) + +- QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs = 455 +- QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs = 253 + +## Files this plan creates + +Each of the eight paths below was probed with `Test-Path` and returned False, so none exists at baseline and none +has a measurable baseline line count. They are recorded as NOT PRESENT rather than as a count of zero, and their +first measurement is made by [P3-T10] after the final CSharpier pass. + +- NOT PRESENT AT BASELINE: UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs +- NOT PRESENT AT BASELINE: UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs +- NOT PRESENT AT BASELINE: QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs +- NOT PRESENT AT BASELINE: UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs +- NOT PRESENT AT BASELINE: UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs +- NOT PRESENT AT BASELINE: UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs +- NOT PRESENT AT BASELINE: UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs +- NOT PRESENT AT BASELINE: QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs + +## PROJECT-FILE (exempt) + +Project files are recorded as exempt observations rather than asserted against the ceiling, per R8: +.claude/rules/general-code-change.md caps production code, test code and reusable script files at 500 lines and +does not reach project files, and `.csharpierignore` lines 9-14 record that project files are owned by Visual +Studio and are not C# source. + +- PROJECT-FILE (exempt): UtilitiesCS/UtilitiesCS.csproj = 1315 +- PROJECT-FILE (exempt): QuickFiler/QuickFiler.csproj = 605 +- PROJECT-FILE (exempt): UtilitiesCS.Test/UtilitiesCS.Test.csproj = 976 +- PROJECT-FILE (exempt): QuickFiler.Test/QuickFiler.Test.csproj = 529 + +## PRE-EXISTING OVER CEILING + +Three files are already over the 500-line ceiling before any change in this plan. They are disclosed here, not +repaired, and are gated by the D11 per-file budgets rather than by the ceiling: + +- UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs = 1003 — in the Write Set. D11 budget: at or below 1003 + (no growth). +- QuickFiler/Controllers/EfcFormController.cs = 1320 — in the Write Set. D11 budget: at or below 1322, that is + baseline plus at most two lines, because the single added constructor argument is formatted by CSharpier as an + additional line. +- UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs = 1066 — NOT touched by this plan. + +## Ceiling-relevant statements required by the acceptance condition + +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is EXACTLY 500 lines, at the ceiling and not merely + near it. This is the file R9's hard ordering constraint protects: [P2-T3] must relocate the breadcrumb pipeline + helper into the new partial before [P2-T14] adds the constructor argument, or the file passes through a 501-line + intermediate state. D11 budget: at or below 500 (hard). +- `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` is 479 lines and therefore has + 21 lines of headroom against the 500-line ceiling. That headroom is why D9 places the no-accessor companion + case in the new file OutlookFolderHierarchyProviderTrimTests.cs rather than in this file, and why the Write Set + gives this file a budget of +4 lines (ceiling 483) for the [P1-T13] retarget. + +Output Summary: All ten existing `.cs` paths have numeric baseline counts, and every count reproduces the figure +the plan's citation table records. Three files are over the ceiling before any change and are disclosed above with +their D11 budgets; the four project files are recorded under the exempt heading with the R8 reason. The two +ceiling-relevant statements the acceptance condition requires are recorded: ViewerSetup.cs is exactly 500, and +OutlookFolderHierarchyProviderTests.cs has 21 lines of headroom. [P3-T10] re-measures these counts after the final +CSharpier pass, because the formatter can change line counts. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t15-tests.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t15-tests.md new file mode 100644 index 000000000..1c51ee191 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t15-tests.md @@ -0,0 +1,54 @@ +# [P0-T15] Pre-change status of the retargeted and must-stay-green tests + +Timestamp: 2026-09-07T07-20 + +Command: TRX `TestDefinitions/UnitTest` to `Results/UnitTestResult` join over the two TRX documents [P0-T11] +wrote, resolving each fully qualified name as `TestMethod/@className` + `.` + `TestMethod/@name` and reading +`UnitTestResult/@outcome` + +EXIT_CODE: 0 + +Source documents (R3-reduced filenames, no TRX content pasted): + +- `TestResults\799-p0-t11-ut\__2026-09-07_06_44_06_net481.trx` +- `TestResults\799-p0-t11-qft\__2026-09-07_06_46_42_net481.trx` + +## The two D9 retarget targets + +BASELINE-PASS: UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTests.GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments +BASELINE-PASS: QuickFiler.Controllers.Tests.QfcItemController_FolderHandlingTests.ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection + +## The D9 recents and projection tests that must stay green + +BASELINE-PASS: UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorTests.FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents +BASELINE-PASS: UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorTests.AddRecents_WhenRecentsExist_AppendsHeaderAndEntries +BASELINE-PASS: UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorTests.Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths +BASELINE-PASS: UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorTests.Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath +BASELINE-PASS: UtilitiesCS.Test.OutlookObjects.Folder.FolderRowTests.FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds +BASELINE-PASS: UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorTests.GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment + +## The ten tests of the partial class BreadcrumbBridgeRouterIssue439Tests (D8 NO HUNK) + +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget +BASELINE-PASS: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests.Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild + +Count: 18 `BASELINE-PASS:` lines, one per named test, 0 `BASELINE-FAIL:` lines. Every one of the 18 named tests was +located in the TRX documents this plan wrote — 7 in the UtilitiesCS.Test document and 11 in the QuickFiler.Test +document — so no line is recorded from an assumption. All ten members of the `BreadcrumbBridgeRouterIssue439Tests` +partial class resolve to the single class name `QuickFiler.Test.Controllers.BreadcrumbBridgeRouterIssue439Tests`, +which spans the base file and the Activation partial. + +Output Summary: All 18 tests pass at the base commit. The set includes +Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome, which pins today's null-chain +selectable-fallback rendering and is exactly the path [P2-T12] modifies, so the modified path is guarded on the +baseline side. Because every entry is `BASELINE-PASS:`, the Phase 2 no-newly-failing comparison reduces to a +requirement that all 18 still pass after the change, with the two D9 retarget targets excepted in the specific +respect that [P1-T13] and [P1-T14] rewrite their assertions. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t2-base.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t2-base.md new file mode 100644 index 000000000..1b24fcce2 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t2-base.md @@ -0,0 +1,17 @@ +# [P0-T2] Branch and base commit + +Timestamp: 2026-09-07T06-38 + +Command: git rev-parse --abbrev-ref HEAD; git rev-parse HEAD; git status --porcelain --untracked-files=all + +EXIT_CODE: 0 + +BASE-BRANCH: bug/breadcrumb-lineage-below-archive-root-799 +BASE-SHA: 2085504e6daaa11b9ec0a8857e7777cf9b10143f + +Output Summary: HEAD of the item worktree is the merge commit that brought origin/main into this branch +immediately before execution began, so every anchored diff later in this plan measures only this item's own +footprint. Porcelain status at the time of capture showed exactly two entries, both produced by [P0-T1] earlier in +this same phase: the modified plan file (its [P0-T1] checkbox) and the untracked +`/evidence/baseline/phase0-instructions-read.md` artifact. No source file is modified. Recorded per R6; +every later `git diff` in this plan binds `$BaseSha` from this artifact's `BASE-SHA` line. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t3-sdk.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t3-sdk.md new file mode 100644 index 000000000..3d766c25f --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t3-sdk.md @@ -0,0 +1,30 @@ +# [P0-T3] Repository-local .NET SDK bootstrap + +Timestamp: 2026-09-07T06-45 + +Command: pwsh -NoProfile -File scripts\vscode\Install-RepoDotNetSdk.ps1 ; then +$env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path ; $env:PATH = "$env:DOTNET_ROOT;$env:PATH" ; +dotnet --version ; Test-Path '.dotnet-sdk\sdk\8.0.205' + +EXIT_CODE: 0 + +EXEC-ENVIRONMENT: pwsh-permitted + +## Before / after existence + +- BEFORE: `.dotnet-sdk` exists = False +- BEFORE: `.dotnet-sdk\sdk\8.0.205` exists = False +- AFTER: `.dotnet-sdk` exists = True +- AFTER: `.dotnet-sdk\sdk\8.0.205` exists = True + +## Printed version + +`dotnet --version` printed `8.0.205` with exit code 0. + +Output Summary: The worktree had no repository-local SDK tree before this task, consistent with the Phase 0 +preamble. The install script downloaded and extracted SDK 8.0.205 into `\.dotnet-sdk` and reported +`Installed repo-local .NET SDK 8.0.205`. After the install, the `global.json`-pinned marker directory +`.dotnet-sdk\sdk\8.0.205` exists and `dotnet --version` prints `8.0.205`, a version beginning `8.0.`, so the pin is +satisfied. The derived line `EXEC-ENVIRONMENT: pwsh-permitted` is recorded per plan rule R11b: this executor +session is not worktree-isolated and PowerShell command blocks ran normally, including this one, so no task in +this plan is blocked by the R11b constraint. Host paths in this artifact are reduced per R3. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t4-restore.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t4-restore.md new file mode 100644 index 000000000..04e87d6fe --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t4-restore.md @@ -0,0 +1,54 @@ +# [P0-T4] NuGet package restore and analyzer HintPath resolution + +Timestamp: 2026-09-07T06-48 + +Command: msbuild TaskMaster.sln /t:Restore /m /p:RestorePackagesConfig=true /p:Configuration=Debug "/p:Platform=Any CPU" +(then the four-project Analyzer Include probe from the [P0-T4] command block) + +EXIT_CODE: 0 + +## Packages subdirectory count + +packages-subdirs before=0 after=172 + +## MSBuild restore summary + +``` +Installed: + 172 package(s) to packages.config projects +Build succeeded. + 0 Warning(s) + 0 Error(s) +``` + +## Analyzer Include HintPath resolution + +40 `` items are declared across the four Write Set project files +(UtilitiesCS 9, QuickFiler 9, UtilitiesCS.Test 11, QuickFiler.Test 11). Every one resolved: + +RESOLVED: 40 +UNRESOLVED: 0 + +Distinct resolved analyzer paths, one per package (each appears in two, three or four of the project files): + +- RESOLVED: ..\packages\Meziantou.Analyzer.3.0.203\analyzers\dotnet\roslyn5.0\cs\Meziantou.Analyzer.dll +- RESOLVED: ..\packages\Roslynator.Analyzers.5.0.0\analyzers\dotnet\roslyn4.7\cs\Roslynator.CSharp.Analyzers.dll +- RESOLVED: ..\packages\Roslynator.Analyzers.5.0.0\analyzers\dotnet\roslyn4.7\cs\Roslynator_Analyzers_Roslynator.Common.dll +- RESOLVED: ..\packages\Roslynator.Analyzers.5.0.0\analyzers\dotnet\roslyn4.7\cs\Roslynator_Analyzers_Roslynator.Core.dll +- RESOLVED: ..\packages\Roslynator.Analyzers.5.0.0\analyzers\dotnet\roslyn4.7\cs\Roslynator_Analyzers_Roslynator.CSharp.dll +- RESOLVED: ..\packages\AsyncFixer.2.1.0\analyzers\dotnet\cs\AsyncFixer.dll +- RESOLVED: ..\packages\Microsoft.CodeAnalysis.BannedApiAnalyzers.5.6.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.BannedApiAnalyzers.dll +- RESOLVED: ..\packages\Microsoft.CodeAnalysis.BannedApiAnalyzers.5.6.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.CSharp.BannedApiAnalyzers.dll +- RESOLVED: ..\packages\SonarAnalyzer.CSharp.10.33.0.1635\analyzers\SonarAnalyzer.CSharp.dll +- RESOLVED: ..\packages\MSTest.Analyzers.4.4.0\analyzers\dotnet\cs\MSTest.Analyzers.dll (test projects only) +- RESOLVED: ..\packages\MSTest.Analyzers.4.4.0\analyzers\dotnet\cs\MSTest.Analyzers.CodeFixes.dll (test projects only) + +Output Summary: The `packages` tree was absent before this task (0 subdirectories), which is the bootstrap +condition the Phase 0 preamble describes rather than a repair. The restore installed 172 packages and MSBuild +reported `Build succeeded` with 0 warnings and 0 errors, exit code 0. The analyzer probe found 40 declared +`` items across the four Write Set project files and resolved all 40 against the restored +packages tree, with zero `UNRESOLVED:` lines, so the pre-planning analyzer version-parity measurement +(Meziantou.Analyzer 3.0.203 and Roslynator.Analyzers 5.0.0) holds in this worktree and no back-fill is required. +CS0006 from an unresolved analyzer path is therefore excluded as a cause of any [P0-T9] or [P0-T10] result. +MSBuild is not on this machine's PATH; the Visual Studio 18 amd64 MSBuild directory was prepended to `PATH` in the +invoking shell so that the command executes in exactly the form the plan states. Host paths reduced per R3. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t5-tools.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t5-tools.md new file mode 100644 index 000000000..e353b3205 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t5-tools.md @@ -0,0 +1,27 @@ +# [P0-T5] Manifest-pinned dotnet tool restore + +Timestamp: 2026-09-07T06-50 + +Command: $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path ; $env:PATH = "$env:DOTNET_ROOT;$env:PATH" ; +dotnet tool restore ; dotnet tool run csharpier --version + +EXIT_CODE: 0 + +## Printed output + +``` +Tool 'csharpier' (version '1.2.6') was restored. Available commands: csharpier + +Restore was successful. +``` + +`dotnet tool run csharpier --version` printed: + +``` +1.2.6 +``` + +Output Summary: `dotnet tool restore` exited 0 and restored the manifest-pinned CSharpier. The version invocation +also exited 0 and printed `1.2.6`, which contains the required substring 1.2.6, so the formatter used by every +later CSharpier task in this plan is the manifest-pinned version rather than a global install. Both commands ran +with `DOTNET_ROOT` and `PATH` re-bound to the repository-local SDK per R11. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t6-dotnet-coverage.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t6-dotnet-coverage.md new file mode 100644 index 000000000..b80f07a5d --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t6-dotnet-coverage.md @@ -0,0 +1,28 @@ +# [P0-T6] dotnet-coverage resolution + +Timestamp: 2026-09-07T06-51 + +Command: $probe = Get-Command dotnet-coverage -ErrorAction SilentlyContinue ; (probe branch taken) ; +dotnet-coverage --version + +EXIT_CODE: 0 + +BRANCH TAKEN: probe branch — `Get-Command dotnet-coverage` returned a command, so the +`dotnet tool install --global dotnet-coverage` branch and its PATH prepend were NOT executed. + +DOTNET-COVERAGE-ON-PATH: true + +## Printed version + +``` +18.10.0+f4cc39224845ffa74bf246c9da2399d50e5d6342 +``` + +The resolved command is `dotnet-coverage.exe`. + +Output Summary: The probe branch is the branch the task predicted for this host. `dotnet-coverage` was already +resolvable with no PATH amendment beyond the repository-local SDK prepend required by R11, and +`dotnet-coverage --version` exited 0 printing version 18.10.0. The tool is available for the D12 coverage form +used by [P0-T12] and by the Phase 3 coverage tasks. The probe was performed with `Get-Command` rather than by +running the tool, because an unresolvable command name raises a PowerShell CommandNotFoundException instead of +setting a non-zero exit code. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t7-vstest.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t7-vstest.md new file mode 100644 index 000000000..6f2551f6e --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t7-vstest.md @@ -0,0 +1,27 @@ +# [P0-T7] vstest.console.exe resolution + +Timestamp: 2026-09-07T06-53 + +Command: $vswhere = "${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 ; +$vstest ; Test-Path $vstest + +EXIT_CODE: 0 + +VSTEST-PATH: C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe + +## Verification + +- `Test-Path` on the resolved path returned `True`, so `VSTEST-PATH` names an existing file. +- vswhere returned exactly one match for the `-find` pattern, so `Select-Object -First 1` did not discard an + alternative installation. +- vswhere exit code 0. When the exit code is read directly after the `| Select-Object -First 1` pipeline it comes + back empty, because `Select-Object -First` stops the upstream pipeline before the native command's exit code is + published. The value above was therefore read from an equivalent invocation that assigns the full vswhere output + first and applies `Select-Object -First 1` afterwards; that invocation resolved the identical single path. + +Output Summary: vstest.console.exe resolves to the Visual Studio 18 Community Test Platform. This is the one +artifact in the plan exempted from R3 path reduction, because pinning the full resolved path is the task's whole +purpose; reduced per R3, the path is +`\Microsoft Visual Studio\18\Community\Common7\IDE\Extensions\TestPlatform\vstest.console.exe`. +Every later task that binds `$vstest` re-runs the two resolution lines above in its own shell, per R11. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t8-csharpier.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t8-csharpier.md new file mode 100644 index 000000000..adf845c7c --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t8-csharpier.md @@ -0,0 +1,22 @@ +# [P0-T8] CSharpier formatting baseline + +Timestamp: 2026-09-07T06-55 + +Command: dotnet tool run csharpier check . + +EXIT_CODE: 0 + +## Verbatim printed line + +``` +Checked 1593 files in 6423ms. +``` + +BASELINE-CSHARPIER-CHECKED-FILES: 1593 + +Output Summary: The read-only CSharpier check exited 0 and printed the single success-case line +`Checked 1593 files in 6423ms.` with no drift entry, so the tree is formatting-clean at the base commit and there +is no disclosed pre-existing drifting-path set to carry forward. `check` returns non-zero on drift, so the exit +code is the gate for this task. The formatter was invoked through `dotnet tool run` against the manifest-pinned +version 1.2.6 confirmed by [P0-T5]. The checked-file count includes the `*.xml` and `packages.config` documents +CSharpier 1.2.6 processes in addition to `*.cs`; project files are excluded by `.csharpierignore`. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t9-analyzers.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t9-analyzers.md new file mode 100644 index 000000000..56219b0cf --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/p0-t9-analyzers.md @@ -0,0 +1,31 @@ +# [P0-T9] Analyzer-build baseline + +Timestamp: 2026-09-07T06-58 + +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true + +EXIT_CODE: 0 + +## MSBuild summary + +``` +Build succeeded. + 0 Warning(s) + 0 Error(s) +``` + +Elapsed 00:00:19.20. Console output captured at default (normal) verbosity, 5247 lines. An independent scan of +the captured output for the literal `: warning ` and `: error ` diagnostic markers found 0 lines of each, which +agrees with the summary counters. + +- WARNINGS: 0 +- ERRORS: 0 + +Output Summary: The analyzer gate is green at the base commit. This is the CLAUDE.md analyzer command exactly, +with `/t:Rebuild` rather than `/t:Build`, so `CoreCompile` ran on every project and the analyzers actually +executed rather than being skipped by MSBuild incrementality. [P0-T4] completed before this task and restored 172 +packages with all 40 `` HintPaths resolved, so the EnsureNuGetPackageBuildImports Error target +that fires at BeforeTargets PrepareForBuild in each of the four Write Set projects could not have fired here: this +result is an analyzer measurement, not a bootstrap outcome, and a bootstrap failure has not been misrecorded as a +red analyzer gate. MSBuild is not on this machine's PATH, so the Visual Studio 18 amd64 MSBuild directory was +prepended to `PATH` in the invoking shell; the command itself is unmodified. Host paths reduced per R3. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/phase0-instructions-read.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/phase0-instructions-read.md new file mode 100644 index 000000000..83d92c8a2 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/baseline/phase0-instructions-read.md @@ -0,0 +1,26 @@ +# [P0-T1] Phase 0 policy read record + +Timestamp: 2026-09-07T06-37 + +Policy Order: policy-compliance-order sequence — (1) CLAUDE.md, (2) .claude/rules/general-code-change.md, +(3) .claude/rules/general-unit-test.md, (4) language-specific rules for the files in scope (C#): +.claude/rules/csharp.md, (5) .claude/rules/tonality.md. + +Command: Read tool applied to each of the five paths below, rooted at the item worktree; line counts measured with +`pwsh -NoProfile -Command "(Get-Content -LiteralPath ).Count"`. + +EXIT_CODE: 0 + +## Files read (in order) + +1. CLAUDE.md — 447 lines +2. .claude/rules/general-code-change.md — 80 lines +3. .claude/rules/general-unit-test.md — 105 lines +4. .claude/rules/csharp.md — 96 lines +5. .claude/rules/tonality.md — 80 lines + +Output Summary: All five policy files exist in the item worktree and were read in full in the +policy-compliance-order sequence. Line counts: 447, 80, 105, 96, 80. Constraints carried into execution: +CSharpier via `dotnet tool run` only; the two MSBuild gate commands use `/t:Rebuild` and must not add +`/p:Nullable=enable`; MSTest + Moq + FluentAssertions for tests; 500-line file ceiling for production, test and +reusable script files; no temporary files in tests; professional and non-hyperbolic tone in all artifacts. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md index c16d60ea9..244dfcdd2 100644 --- a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md @@ -384,9 +384,9 @@ EnsureNuGetPackageBuildImports target whose Error fires at BeforeTargets Prepare (`UtilitiesCS/UtilitiesCS.csproj`:1293, `QuickFiler/QuickFiler.csproj`:586, `UtilitiesCS.Test/UtilitiesCS.Test.csproj`:946, `QuickFiler.Test/QuickFiler.Test.csproj`:501), which is a bootstrap failure and nothing else. -- [ ] [P0-T1] Read, in the `policy-compliance-order` sequence, `CLAUDE.md`, .claude/rules/general-code-change.md, .claude/rules/general-unit-test.md, .claude/rules/csharp.md, and .claude/rules/tonality.md, then write `/evidence/baseline/phase0-instructions-read.md` containing the literal field lines `Timestamp:`, `Policy Order:`, and an explicit list of the five files read with their line counts. Acceptance: the artifact exists and contains all five paths and the three field lines. +- [x] [P0-T1] Read, in the `policy-compliance-order` sequence, `CLAUDE.md`, .claude/rules/general-code-change.md, .claude/rules/general-unit-test.md, .claude/rules/csharp.md, and .claude/rules/tonality.md, then write `/evidence/baseline/phase0-instructions-read.md` containing the literal field lines `Timestamp:`, `Policy Order:`, and an explicit list of the five files read with their line counts. Acceptance: the artifact exists and contains all five paths and the three field lines. -- [ ] [P0-T2] Record the branch and base commit into `/evidence/baseline/p0-t2-base.md`, including the literal field lines `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`, and the two derived lines `BASE-BRANCH: ` and `BASE-SHA: <40-hex>`. Acceptance: both derived lines are present and `BASE-SHA` is a 40-character lowercase hexadecimal value. +- [x] [P0-T2] Record the branch and base commit into `/evidence/baseline/p0-t2-base.md`, including the literal field lines `Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`, and the two derived lines `BASE-BRANCH: ` and `BASE-SHA: <40-hex>`. Acceptance: both derived lines are present and `BASE-SHA` is a 40-character lowercase hexadecimal value. ```powershell git rev-parse --abbrev-ref HEAD @@ -394,7 +394,7 @@ git rev-parse HEAD git status --porcelain --untracked-files=all ``` -- [ ] [P0-T3] Install the repository-local .NET SDK with scripts\vscode\Install-RepoDotNetSdk.ps1 and record `/evidence/baseline/p0-t3-sdk.md`. `global.json` pins SDK 8.0.205 with the search paths ".dotnet-sdk" then "$host$", and no host SDK on this machine satisfies that pin, so every `dotnet` command fails until this task completes. Record the existence of the .dotnet-sdk directory BEFORE and AFTER the command, so the artifact is truthful whether the tree was absent or already present. Acceptance: after the command, the directory .dotnet-sdk\sdk\8.0.205 exists and `dotnet --version` prints a version beginning `8.0.`; the artifact records both the before/after existence booleans and the printed version. +- [x] [P0-T3] Install the repository-local .NET SDK with scripts\vscode\Install-RepoDotNetSdk.ps1 and record `/evidence/baseline/p0-t3-sdk.md`. `global.json` pins SDK 8.0.205 with the search paths ".dotnet-sdk" then "$host$", and no host SDK on this machine satisfies that pin, so every `dotnet` command fails until this task completes. Record the existence of the .dotnet-sdk directory BEFORE and AFTER the command, so the artifact is truthful whether the tree was absent or already present. Acceptance: after the command, the directory .dotnet-sdk\sdk\8.0.205 exists and `dotnet --version` prints a version beginning `8.0.`; the artifact records both the before/after existence booleans and the printed version. ```powershell pwsh -NoProfile -File scripts\vscode\Install-RepoDotNetSdk.ps1 @@ -404,7 +404,7 @@ dotnet --version Test-Path '.dotnet-sdk\sdk\8.0.205' ``` -- [ ] [P0-T4] Restore NuGet packages for the solution and record `/evidence/baseline/p0-t4-restore.md`. The packages tree is not present in this worktree, so this is a bootstrap step and not a repair. Record the count of packages subdirectories before and after the command, and the resolution status of every Analyzer Include HintPath declared by the four Write Set project files, because an unresolved analyzer path is CS0006, an error, and would fail [P0-T9] and [P0-T10] for a reason unrelated to this change. Analyzer version parity between the project files and packages.config was measured as clean before planning (Meziantou.Analyzer 3.0.203 and Roslynator.Analyzers 5.0.0 on both sides), so this probe is a verification step and no back-fill is planned. Acceptance: the artifact records the restore `EXIT_CODE:`, the before and after subdirectory counts, and one `RESOLVED:` or `UNRESOLVED:` line per analyzer path with zero `UNRESOLVED:` lines. +- [x] [P0-T4] Restore NuGet packages for the solution and record `/evidence/baseline/p0-t4-restore.md`. The packages tree is not present in this worktree, so this is a bootstrap step and not a repair. Record the count of packages subdirectories before and after the command, and the resolution status of every Analyzer Include HintPath declared by the four Write Set project files, because an unresolved analyzer path is CS0006, an error, and would fail [P0-T9] and [P0-T10] for a reason unrelated to this change. Analyzer version parity between the project files and packages.config was measured as clean before planning (Meziantou.Analyzer 3.0.203 and Roslynator.Analyzers 5.0.0 on both sides), so this probe is a verification step and no back-fill is planned. Acceptance: the artifact records the restore `EXIT_CODE:`, the before and after subdirectory counts, and one `RESOLVED:` or `UNRESOLVED:` line per analyzer path with zero `UNRESOLVED:` lines. ```powershell $before = if (Test-Path 'packages') { (Get-ChildItem -Path 'packages' -Directory).Count } else { 0 } @@ -421,7 +421,7 @@ foreach ($proj in @('UtilitiesCS\UtilitiesCS.csproj', 'QuickFiler\QuickFiler.csp } ``` -- [ ] [P0-T5] Restore the manifest-pinned dotnet tools and record `/evidence/baseline/p0-t5-tools.md`. Acceptance: the artifact records `EXIT_CODE: 0` and the printed version string contains the substring 1.2.6. +- [x] [P0-T5] Restore the manifest-pinned dotnet tools and record `/evidence/baseline/p0-t5-tools.md`. Acceptance: the artifact records `EXIT_CODE: 0` and the printed version string contains the substring 1.2.6. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path @@ -430,7 +430,7 @@ dotnet tool restore dotnet tool run csharpier --version ``` -- [ ] [P0-T6] Resolve `dotnet-coverage` and record `/evidence/baseline/p0-t6-dotnet-coverage.md`. Probe with `Get-Command dotnet-coverage -ErrorAction SilentlyContinue` rather than by running the tool, because an unresolvable command name raises a PowerShell CommandNotFoundException instead of setting a non-zero exit code, so there is no exit code for a branch condition to read. Only when the probe returns nothing, run `dotnet tool install --global dotnet-coverage`, then prepend the user-profile global-tool directory to PATH and re-probe; that prepend is required only on this branch, because a shell that was already running when the tool was installed does not inherit the new directory. On this host the probe branch is the expected one: the completed issue #791 run recorded the tool resolving with no PATH amendment. Acceptance: the artifact records a final `dotnet-coverage --version` invocation with `EXIT_CODE: 0` and the printed version string, states which of the two branches was taken, and records the derived line `DOTNET-COVERAGE-ON-PATH: true`. +- [x] [P0-T6] Resolve `dotnet-coverage` and record `/evidence/baseline/p0-t6-dotnet-coverage.md`. Probe with `Get-Command dotnet-coverage -ErrorAction SilentlyContinue` rather than by running the tool, because an unresolvable command name raises a PowerShell CommandNotFoundException instead of setting a non-zero exit code, so there is no exit code for a branch condition to read. Only when the probe returns nothing, run `dotnet tool install --global dotnet-coverage`, then prepend the user-profile global-tool directory to PATH and re-probe; that prepend is required only on this branch, because a shell that was already running when the tool was installed does not inherit the new directory. On this host the probe branch is the expected one: the completed issue #791 run recorded the tool resolving with no PATH amendment. Acceptance: the artifact records a final `dotnet-coverage --version` invocation with `EXIT_CODE: 0` and the printed version string, states which of the two branches was taken, and records the derived line `DOTNET-COVERAGE-ON-PATH: true`. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path @@ -443,7 +443,7 @@ if ($null -eq $probe) { dotnet-coverage --version ``` -- [ ] [P0-T7] Resolve vstest.console.exe through vswhere and record the full resolved path into `/evidence/baseline/p0-t7-vstest.md` as the derived line `VSTEST-PATH: `. This is the one artifact exempted from R3's path reduction, because pinning the resolved path is the task's purpose. Acceptance: `VSTEST-PATH` names an existing file and the artifact records `EXIT_CODE: 0`. +- [x] [P0-T7] Resolve vstest.console.exe through vswhere and record the full resolved path into `/evidence/baseline/p0-t7-vstest.md` as the derived line `VSTEST-PATH: `. This is the one artifact exempted from R3's path reduction, because pinning the resolved path is the task's purpose. Acceptance: `VSTEST-PATH` names an existing file and the artifact records `EXIT_CODE: 0`. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -452,7 +452,7 @@ $vstest Test-Path $vstest ``` -- [ ] [P0-T8] Capture the CSharpier baseline into `/evidence/baseline/p0-t8-csharpier.md`, recording the verbatim printed line and the derived line `BASELINE-CSHARPIER-CHECKED-FILES: `. The success-case output of this command on a clean tree is the single line of the form `Checked files in ms.` with exit 0; `check` is read-only and returns non-zero on drift, so the exit code is the gate here. If the check reports drift, the artifact must list every drifting path as a disclosed pre-existing set. Acceptance: the artifact records `EXIT_CODE:`, the printed line, and the `BASELINE-CSHARPIER-CHECKED-FILES` numeral. +- [x] [P0-T8] Capture the CSharpier baseline into `/evidence/baseline/p0-t8-csharpier.md`, recording the verbatim printed line and the derived line `BASELINE-CSHARPIER-CHECKED-FILES: `. The success-case output of this command on a clean tree is the single line of the form `Checked files in ms.` with exit 0; `check` is read-only and returns non-zero on drift, so the exit code is the gate here. If the check reports drift, the artifact must list every drifting path as a disclosed pre-existing set. Acceptance: the artifact records `EXIT_CODE:`, the printed line, and the `BASELINE-CSHARPIER-CHECKED-FILES` numeral. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path @@ -460,19 +460,19 @@ $env:PATH = "$env:DOTNET_ROOT;$env:PATH" dotnet tool run csharpier check . ``` -- [ ] [P0-T9] Capture the analyzer-build baseline into `/evidence/baseline/p0-t9-analyzers.md` using exactly the CLAUDE.md analyzer command. Acceptance: the artifact records `EXIT_CODE:` and an `Output Summary:` giving the warning and error counts read from the MSBuild summary, and states explicitly that [P0-T4] completed first so a bootstrap failure cannot be misread as an analyzer failure. +- [x] [P0-T9] Capture the analyzer-build baseline into `/evidence/baseline/p0-t9-analyzers.md` using exactly the CLAUDE.md analyzer command. Acceptance: the artifact records `EXIT_CODE:` and an `Output Summary:` giving the warning and error counts read from the MSBuild summary, and states explicitly that [P0-T4] completed first so a bootstrap failure cannot be misread as an analyzer failure. ```powershell msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true ``` -- [ ] [P0-T10] Capture the nullable-build baseline into `/evidence/baseline/p0-t10-nullable.md` using exactly the CLAUDE.md nullable command. `/p:Nullable=enable` must not be added and `/t:Build` must not be substituted. Acceptance: the artifact records `EXIT_CODE:` and the warning and error counts. +- [x] [P0-T10] Capture the nullable-build baseline into `/evidence/baseline/p0-t10-nullable.md` using exactly the CLAUDE.md nullable command. `/p:Nullable=enable` must not be added and `/t:Build` must not be substituted. Acceptance: the artifact records `EXIT_CODE:` and the warning and error counts. ```powershell msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true ``` -- [ ] [P0-T11] Run the UtilitiesCS.Test and QuickFiler.Test assemblies and record their pass/fail counts into `/evidence/baseline/p0-t11-suites.md` as the derived lines `BASELINE-UT-TOTAL:`, `BASELINE-UT-PASSED:`, `BASELINE-UT-FAILED:`, `BASELINE-QFT-TOTAL:`, `BASELINE-QFT-PASSED:`, `BASELINE-QFT-FAILED:`, read from the TRX `ResultSummary/Counters` element of each run. Do not paste TRX content (R3). Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: all six derived lines are present and numeric, each `BASELINE-*-FAILED` recorded whatever its value, and the artifact names the four excluded shell-icon classes (R13); the artifact additionally records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema, and each `BASELINE-*-FAILED` value is read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run. +- [x] [P0-T11] Run the UtilitiesCS.Test and QuickFiler.Test assemblies and record their pass/fail counts into `/evidence/baseline/p0-t11-suites.md` as the derived lines `BASELINE-UT-TOTAL:`, `BASELINE-UT-PASSED:`, `BASELINE-UT-FAILED:`, `BASELINE-QFT-TOTAL:`, `BASELINE-QFT-PASSED:`, `BASELINE-QFT-FAILED:`, read from the TRX `ResultSummary/Counters` element of each run. Do not paste TRX content (R3). Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: all six derived lines are present and numeric, each `BASELINE-*-FAILED` recorded whatever its value, and the artifact names the four excluded shell-icon classes (R13); the artifact additionally records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema, and each `BASELINE-*-FAILED` value is read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -481,7 +481,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p0-t11-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook' ``` -- [ ] [P0-T12] Run the full nine-assembly suite under `dotnet-coverage` and record `/evidence/baseline/p0-t12-coverage.md` with the derived lines `BASELINE-LINES-COVERED:`, `BASELINE-LINES-VALID:`, `BASELINE-BRANCHES-COVERED:`, `BASELINE-BRANCHES-VALID:`, `BASELINE-PACKAGES-MATCHED:`, the two derived percentages, and `BASELINE-TOTAL-TESTS:` / `BASELINE-FAILED-TESTS:`. The four counters come from the pinned aggregation block below, whose success-case output is the single line it prints itself, of the form `LINES_COVERED= LINES_VALID= BRANCHES_COVERED= BRANCHES_VALID= PACKAGES_MATCHED=` (D13). Record `BASELINE_FLOOR: MET` or `BASELINE_FLOOR: NOT MET` against the 80 percent line floor, explicitly qualified as measured against the D13 comparability index rather than against the repository line-coverage rate, and continue either way; a pre-existing repository floor never halts this plan. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: the five `BASELINE-` counter lines are present and numeric and `BASELINE-PACKAGES-MATCHED` is greater than zero. +- [x] [P0-T12] Run the full nine-assembly suite under `dotnet-coverage` and record `/evidence/baseline/p0-t12-coverage.md` with the derived lines `BASELINE-LINES-COVERED:`, `BASELINE-LINES-VALID:`, `BASELINE-BRANCHES-COVERED:`, `BASELINE-BRANCHES-VALID:`, `BASELINE-PACKAGES-MATCHED:`, the two derived percentages, and `BASELINE-TOTAL-TESTS:` / `BASELINE-FAILED-TESTS:`. The four counters come from the pinned aggregation block below, whose success-case output is the single line it prints itself, of the form `LINES_COVERED= LINES_VALID= BRANCHES_COVERED= BRANCHES_VALID= PACKAGES_MATCHED=` (D13). Record `BASELINE_FLOOR: MET` or `BASELINE_FLOOR: NOT MET` against the 80 percent line floor, explicitly qualified as measured against the D13 comparability index rather than against the repository line-coverage rate, and continue either way; a pre-existing repository floor never halts this plan. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: the five `BASELINE-` counter lines are present and numeric and `BASELINE-PACKAGES-MATCHED` is greater than zero. ```powershell $env:DOTNET_ROOT = (Resolve-Path '.dotnet-sdk').Path @@ -514,7 +514,7 @@ foreach ($pkg in $doc.SelectNodes('//package')) { "LINES_COVERED=$lc LINES_VALID=$lv BRANCHES_COVERED=$bc BRANCHES_VALID=$bv PACKAGES_MATCHED=$pm" ``` -- [ ] [P0-T13] Determine, from coverage\799-baseline.cobertura.xml, which Write Set production files are measurable, and write `/evidence/baseline/p0-t13-measurability.md`. For each of the six EXISTING Write Set production paths, and additionally for UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs, query for a `class` element whose `filename` attribute ends with a directory separator followed by that file's name, and record one line per file of the form `MEASURABLE: ` or `UNMEASURABLE: `. The trailing-name match must be separator-anchored, because an unanchored suffix over-selects a sibling whose name merely ends with the same characters. The two files this plan CREATES are recorded separately as `NEW: ` and are measured for the first time by [P3-T9]. Acceptance: exactly seven `MEASURABLE:`/`UNMEASURABLE:` lines are present — one for each of the six EXISTING Write Set production paths, plus one for UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs, which this plan does not modify but whose measurability is recorded because [P2-T1], [P2-T2] and [P2-T9] all route through it and a zero-class-element result there would explain an otherwise puzzling [P3-T7] outcome — and the artifact records the class-element counts the determination was made from. +- [x] [P0-T13] Determine, from coverage\799-baseline.cobertura.xml, which Write Set production files are measurable, and write `/evidence/baseline/p0-t13-measurability.md`. For each of the six EXISTING Write Set production paths, and additionally for UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs, query for a `class` element whose `filename` attribute ends with a directory separator followed by that file's name, and record one line per file of the form `MEASURABLE: ` or `UNMEASURABLE: `. The trailing-name match must be separator-anchored, because an unanchored suffix over-selects a sibling whose name merely ends with the same characters. The two files this plan CREATES are recorded separately as `NEW: ` and are measured for the first time by [P3-T9]. Acceptance: exactly seven `MEASURABLE:`/`UNMEASURABLE:` lines are present — one for each of the six EXISTING Write Set production paths, plus one for UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs, which this plan does not modify but whose measurability is recorded because [P2-T1], [P2-T2] and [P2-T9] all route through it and a zero-class-element result there would explain an otherwise puzzling [P3-T7] outcome — and the artifact records the class-element counts the determination was made from. ```powershell $doc = New-Object System.Xml.XmlDocument @@ -530,9 +530,9 @@ foreach ($n in $names) { } ``` -- [ ] [P0-T14] Record the baseline line count of every file this plan edits or creates into `/evidence/baseline/p0-t14-sizes.md`, one ` = ` line per file, covering the six existing Write Set production paths, the two retargeted Write Set test paths, and the two #439 test paths D8 marks NO HUNK, plus a `CEILING: 500 (applies to *.cs only)` line. Record the four project files separately under a `PROJECT-FILE (exempt): = ` heading with the R8 reason. Record the three pre-existing over-ceiling files under a `PRE-EXISTING OVER CEILING:` heading with the D11 budgets. Acceptance: every `.cs` path has a numeric count; the four project-file counts are recorded under the exempt heading with their reason; the artifact states that `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is exactly 500 and that `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` has 21 lines of headroom. +- [x] [P0-T14] Record the baseline line count of every file this plan edits or creates into `/evidence/baseline/p0-t14-sizes.md`, one ` = ` line per file, covering the six existing Write Set production paths, the two retargeted Write Set test paths, and the two #439 test paths D8 marks NO HUNK, plus a `CEILING: 500 (applies to *.cs only)` line. Record the four project files separately under a `PROJECT-FILE (exempt): = ` heading with the R8 reason. Record the three pre-existing over-ceiling files under a `PRE-EXISTING OVER CEILING:` heading with the D11 budgets. Acceptance: every `.cs` path has a numeric count; the four project-file counts are recorded under the exempt heading with their reason; the artifact states that `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is exactly 500 and that `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` has 21 lines of headroom. -- [ ] [P0-T15] Record the pre-change status of the tests this plan retargets or must keep green into `/evidence/baseline/p0-t15-tests.md`, one line per test of the form `BASELINE-PASS: ` or `BASELINE-FAIL: `, derived from the two TRX documents [P0-T11] wrote. The set is: the two retarget targets named in D9 (GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments and ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection); the recents and projection tests named in D9 that must stay green (FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents, AddRecents_WhenRecentsExist_AppendsHeaderAndEntries, Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths, Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath, FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds, GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment); and the ten tests of the partial class `BreadcrumbBridgeRouterIssue439Tests`, across both files, which D8 marks NO HUNK (Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability, Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively, Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome, Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows, Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic, Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection, Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget, Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget, Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget and Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild). The third of those is the one that pins today's null-chain selectable-fallback rendering, which is exactly the path [P2-T12] modifies, so omitting it would leave the modified path unguarded. This is the set that makes the Phase 2 no-newly-failing comparison meaningful. Acceptance: eighteen `BASELINE-PASS:` or `BASELINE-FAIL:` lines are present, one per named test, each derived from a TRX this plan wrote. +- [x] [P0-T15] Record the pre-change status of the tests this plan retargets or must keep green into `/evidence/baseline/p0-t15-tests.md`, one line per test of the form `BASELINE-PASS: ` or `BASELINE-FAIL: `, derived from the two TRX documents [P0-T11] wrote. The set is: the two retarget targets named in D9 (GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments and ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection); the recents and projection tests named in D9 that must stay green (FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents, AddRecents_WhenRecentsExist_AppendsHeaderAndEntries, Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths, Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath, FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds, GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment); and the ten tests of the partial class `BreadcrumbBridgeRouterIssue439Tests`, across both files, which D8 marks NO HUNK (Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability, Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively, Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome, Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows, Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic, Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection, Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget, Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget, Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget and Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild). The third of those is the one that pins today's null-chain selectable-fallback rendering, which is exactly the path [P2-T12] modifies, so omitting it would leave the modified path unguarded. This is the set that makes the Phase 2 no-newly-failing comparison meaningful. Acceptance: eighteen `BASELINE-PASS:` or `BASELINE-FAIL:` lines are present, one per named test, each derived from a TRX this plan wrote. --- From e601ecfbfbb573e479fc3163f7f3bc60fdb14647 Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Mon, 7 Sep 2026 07:19:14 -0400 Subject: [PATCH 09/13] test(799): Phase 1 declaration seams and failing regression tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the issue #799 declaration seams and the test-first regression set that Phase 2 must turn green. No production behaviour changes in this commit: every seam body either throws NotImplementedException or stores a value nothing reads. Seams: - ArchiveStemProjection.ToDisplayStem — lenient display projection (AC4-AC6) - ArchiveChainProjection.TryTrimBelowArchiveRoot — chain trim (AC1, AC2) - OutlookFolderHierarchyProvider: IFolderLabelAbsenceReport, optional lazy archive-root accessor, and an injected diagnostic error sink (AC7) Tests: 4 new UtilitiesCS.Test classes, 1 new QuickFiler.Test class, and 2 retargeted tests whose expectations encoded the empty-root separator strip that AC4 eliminates. Evidence: 33 tests are red at the end of Phase 1 (19 SEAM-BLOCKED, 11 NEW, 3 RETARGETED); both fail-before runs exit 1 as intended. Both Phase 1 builds exit 0 with 0 warnings and 0 errors, and CS8625-COUNT and CS8618-COUNT are both zero. Co-Authored-By: Claude Fable 5.1 --- .../BreadcrumbBridgeRouterScoreJoinTests.cs | 425 ++++++++++++++++++ ...temController.FolderHandlingTests.Part2.cs | 33 +- QuickFiler.Test/QuickFiler.Test.csproj | 1 + .../Folder/ArchiveChainProjectionTests.cs | 217 +++++++++ .../Folder/ArchiveStemProjectionTests.cs | 176 ++++++++ .../FolderPredictorRecentsProjectionTests.cs | 213 +++++++++ .../OutlookFolderHierarchyProviderTests.cs | 9 +- ...OutlookFolderHierarchyProviderTrimTests.cs | 344 ++++++++++++++ UtilitiesCS.Test/UtilitiesCS.Test.csproj | 4 + .../Folder/ArchiveChainProjection.cs | 54 +++ .../Folder/ArchiveStemProjection.cs | 48 ++ .../Folder/OutlookFolderHierarchyProvider.cs | 63 ++- UtilitiesCS/UtilitiesCS.csproj | 2 + .../regression-testing/p1-t15-test-build.md | 54 +++ .../regression-testing/p1-t16-ut-fail.md | 108 +++++ .../regression-testing/p1-t17-qft-fail.md | 88 ++++ .../p1-t18-red-inventory.md | 83 ++++ .../regression-testing/p1-t5-seam-build.md | 56 +++ .../plan.2026-09-06T22-01.md | 36 +- 19 files changed, 1978 insertions(+), 36 deletions(-) create mode 100644 QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs create mode 100644 UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs create mode 100644 UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs create mode 100644 UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs create mode 100644 UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs create mode 100644 UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs create mode 100644 UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t15-test-build.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t16-ut-fail.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t17-qft-fail.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t18-red-inventory.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t5-seam-build.md diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs new file mode 100644 index 000000000..90558860c --- /dev/null +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs @@ -0,0 +1,425 @@ +using System; +using System.Collections.Generic; +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 +{ + /// + /// Headless issue #799 regression tests for the breadcrumb router's score join (AC6), the + /// archive-relative filing invariant under a trimmed chain (AC3), and the zero-candidate row + /// suppression (AC7). Every collaborator is a Moq seam or a real pure type; this file creates + /// no WebView2 control, no Outlook COM object, and no message pump. + /// + /// Authored for C# 7.3. QuickFiler.Test declares no LangVersion element and targets v4.8.1, so + /// it compiles at the 7.3 default while every other project in scope is at Latest, preview, or + /// 12.0. Do not introduce target-typed new, is not null, switch expressions, or + /// nullable reference annotations here: they surface as CS8370 at build time, not at edit time. + /// + /// + [TestClass] + public sealed class BreadcrumbBridgeRouterScoreJoinTests + { + private const string ArchiveRoot = @"\Archive"; + private const string RelativeTarget = @"Clients\North"; + private const string RootedTarget = @"\Archive\Clients\North"; + private const string ClientsPath = @"\Archive\Clients"; + + /// + /// AC6: the Efc surface hands the router the RAW score paths, which are archive-rooted, + /// while the presented rows are archive-relative stems. The join must still find the score, + /// or an archive-rooted suggestion silently loses its percentage. + /// + [TestMethod] + public void BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage() + { + // Arrange + var documents = new List(); + var provider = new Mock(MockBehavior.Strict); + var host = StrictHost(documents); + FolderTreeNodeKey key = Key(RootedTarget); + SetupChain(provider, RootedTarget, key, TwoSegmentChain(ClientsPath, "Clients", + RootedTarget, "North")); + var router = RouterOver(provider, host); + + // Act: presented row is the stem; the score carries the rooted path. + router + .BindRowsAsync( + new[] { RelativeTarget }, + new[] { new FolderScore(RootedTarget, 730, 0.73) }, + ArchiveRoot, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + + // Assert + documents.Should().ContainSingle(); + documents[0].Should().Contain("73%", "an archive-rooted score must still join"); + } + + /// + /// Decision D7: the projected score key is ADDED alongside the raw key, never substituted + /// for it. A substitution would fix the stem-presented case and silently break this one. + /// + [TestMethod] + public void BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage() + { + // Arrange + var documents = new List(); + var provider = new Mock(MockBehavior.Strict); + var host = StrictHost(documents); + FolderTreeNodeKey key = Key(RootedTarget); + SetupChain(provider, RootedTarget, key, TwoSegmentChain(ClientsPath, "Clients", + RootedTarget, "North")); + var router = RouterOver(provider, host); + + // Act: presented row and score both carry the rooted path. + router + .BindRowsAsync( + new[] { RootedTarget }, + new[] { new FolderScore(RootedTarget, 730, 0.73) }, + ArchiveRoot, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + + // Assert + documents.Should().ContainSingle(); + documents[0].Should().Contain("73%", "the rooted-presented case must not regress"); + } + + /// + /// The public three-argument overload forwards an empty archive root, so the projection is + /// the identity and no existing caller changes behaviour. + /// + [TestMethod] + public void BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged() + { + // Arrange + var documents = new List(); + var provider = new Mock(MockBehavior.Strict); + var host = StrictHost(documents); + FolderTreeNodeKey key = Key(RelativeTarget); + SetupChain(provider, RelativeTarget, key, TwoSegmentChain(@"Clients", "Clients", + RelativeTarget, "North")); + var router = RouterOver(provider, host); + + // Act: the public overload, which supplies no archive root at all. + router + .BindRowsAsync( + new[] { RelativeTarget }, + new[] { new FolderScore(RelativeTarget, 730, 0.73) }, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + + // Assert + documents.Should().ContainSingle(); + documents[0].Should().Contain("73%"); + } + + /// + /// AC3, pinned as its own criterion rather than as an incidental consequence of the trim: + /// with an ancestor chain that begins BELOW the archive root, the filing target and the + /// joined score key are both still the archive-relative stem. + /// + [TestMethod] + public void BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey() + { + // Arrange: the chain carries no store segment and no Archive segment. + var documents = new List(); + var provider = new Mock(MockBehavior.Strict); + var host = StrictHost(documents); + FolderTreeNodeKey key = Key(RootedTarget); + SetupChain(provider, RootedTarget, key, TwoSegmentChain(ClientsPath, "Clients", + RootedTarget, "North")); + var router = RouterOver(provider, host); + + // Act + router + .BindRowsAsync( + new[] { RelativeTarget }, + new[] { new FolderScore(RootedTarget, 730, 0.73) }, + ArchiveRoot, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync("{\"type\":\"rowSelected\",\"rowId\":\"row-0\"}") + .GetAwaiter() + .GetResult(); + + // Assert + router + .SelectedFolderPath.Should() + .Be(RelativeTarget, "the filing target stays archive-relative (#439)"); + documents[documents.Count - 1] + .Should() + .Contain("73%", "the score key stays joined to the archive-relative stem"); + } + + /// + /// The spec's integration scenario, driven entirely through the router: a banner row, a + /// suggestion row, a search-result row, the trash pseudo-row, and one stale label that the + /// provider cannot resolve. Lineage renders on both folder row kinds only. + /// + [TestMethod] + public void BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly() + { + // Arrange + const string searchTarget = @"Search\Follow Up"; + const string searchRooted = @"\Archive\Search\Follow Up"; + const string staleTarget = @"Clients\Stale"; + var documents = new List(); + var provider = new Mock(MockBehavior.Strict); + var host = StrictHost(documents); + FolderTreeNodeKey suggestionKey = Key(RootedTarget); + FolderTreeNodeKey searchKey = Key(searchRooted); + SetupChain(provider, RootedTarget, suggestionKey, TwoSegmentChain(ClientsPath, + "Clients", RootedTarget, "North")); + SetupChain(provider, searchRooted, searchKey, TwoSegmentChain(@"\Archive\Search", + "Search", searchRooted, "Follow Up")); + provider + .Setup(p => + p.ResolveLeafKeyAsync(@"\Archive\Clients\Stale", It.IsAny()) + ) + .ReturnsAsync((FolderTreeNodeKey)null); + var router = RouterOver(provider, host); + + // Act + router + .BindRowsAsync( + new[] + { + "==== SUGGESTIONS ====", + RelativeTarget, + searchTarget, + "Trash to Delete", + staleTarget, + }, + new[] { new FolderScore(RootedTarget, 730, 0.73) }, + ArchiveRoot, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + + // Assert: exactly two rows render an ancestor separator, and they are the two folder + // row kinds; the banner, the trash pseudo-row and the stale label render none. + string document = documents[0]; + Occurrences(document, "class=\"sep\"") + .Should() + .Be(2, "lineage renders on the suggestion and search rows only"); + document.Should().Contain("title=\"" + ClientsPath + "\""); + document.Should().Contain("title=\"\\Archive\\Search\""); + document.Should().Contain("row banner"); + document.Should().Contain("row selectable trash"); + document.Should().Contain(">Stale<", "the stale label keeps the leaf-only fallback"); + } + + /// + /// AC7 row half, and the only test here that takes the TRUE arm of the suppression + /// predicate. The suppressed row sits in the MIDDLE of the presented sequence, so a + /// suppression that removed the row from the built list without removing it from the + /// presented list would misalign every later row's segment keys. + /// + [TestMethod] + public void BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned() + { + // Arrange + const string vendorsTarget = @"Vendors\South"; + const string vendorsRooted = @"\Archive\Vendors\South"; + const string vendorsPath = @"\Archive\Vendors"; + const string staleTarget = @"Clients\Stale"; + const string staleRooted = @"\Archive\Clients\Stale"; + var documents = new List(); + var provider = new Mock(MockBehavior.Strict); + + // Moq throws when an interface is added to a mock whose object already exists, so the + // As<> call precedes every use of provider.Object. + var absence = provider.As(); + absence.Setup(a => a.IsAbsentLabel(It.IsAny())).Returns(false); + absence.Setup(a => a.IsAbsentLabel(staleRooted)).Returns(true); + absence.Setup(a => a.IsAbsentLabel(staleTarget)).Returns(true); + + var host = StrictHost(documents); + FolderTreeNodeKey northKey = Key(RootedTarget); + FolderTreeNodeKey southKey = Key(vendorsRooted); + FolderTreeNodeKey vendorsKey = Key(vendorsPath); + SetupChain(provider, RootedTarget, northKey, TwoSegmentChain(ClientsPath, "Clients", + RootedTarget, "North")); + SetupChain(provider, vendorsRooted, southKey, TwoSegmentChain(vendorsPath, "Vendors", + vendorsRooted, "South")); + provider + .Setup(p => p.ResolveLeafKeyAsync(staleRooted, It.IsAny())) + .ReturnsAsync((FolderTreeNodeKey)null); + provider + .Setup(p => p.GetImmediateSubfoldersAsync(vendorsKey, It.IsAny())) + .ReturnsAsync(new[] { Segment(vendorsRooted, "South", false) }); + var router = RouterOver(provider, host); + + // Act + router + .BindRowsAsync( + new[] { "==== SUGGESTIONS ====", RelativeTarget, staleTarget, vendorsTarget }, + new[] { new FolderScore(RootedTarget, 730, 0.73) }, + ArchiveRoot, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync( + "{\"type\":\"segmentActivate\",\"rowId\":\"row-2\",\"segmentIndex\":0}" + ) + .GetAwaiter() + .GetResult(); + router + .ProcessInboundAsync("{\"type\":\"leafExpandToggle\",\"rowId\":\"row-2\"}") + .GetAwaiter() + .GetResult(); + + // Assert + string document = documents[0]; + document.Should().NotContain("Stale", "the zero-candidate label is suppressed"); + Occurrences(document, "data-row-id=\"row-") + .Should() + .Be(3, "one presented row of four was suppressed"); + provider.Verify( + p => p.GetImmediateSubfoldersAsync(vendorsKey, It.IsAny()), + Times.Once + ); + } + + /// + /// Decision D-B restricts suppression to the zero-candidate cause. An ambiguous label also + /// yields a null chain, but it is not absent and must still render with today's fallback. + /// + [TestMethod] + public void BindRowsAsync_AmbiguousLabel_IsNotSuppressed() + { + // Arrange + const string ambiguousTarget = @"Clients\Stale"; + const string ambiguousRooted = @"\Archive\Clients\Stale"; + var documents = new List(); + var provider = new Mock(MockBehavior.Strict); + var absence = provider.As(); + absence.Setup(a => a.IsAbsentLabel(It.IsAny())).Returns(false); + + var host = StrictHost(documents); + FolderTreeNodeKey northKey = Key(RootedTarget); + SetupChain(provider, RootedTarget, northKey, TwoSegmentChain(ClientsPath, "Clients", + RootedTarget, "North")); + provider + .Setup(p => p.ResolveLeafKeyAsync(ambiguousRooted, It.IsAny())) + .ReturnsAsync((FolderTreeNodeKey)null); + var router = RouterOver(provider, host); + + // Act + router + .BindRowsAsync( + new[] { RelativeTarget, ambiguousTarget }, + new[] { new FolderScore(RootedTarget, 730, 0.73) }, + ArchiveRoot, + CancellationToken.None + ) + .GetAwaiter() + .GetResult(); + + // Assert + string document = documents[0]; + document.Should().Contain(">Stale<", "ambiguity is not absence"); + Occurrences(document, "data-row-id=\"row-").Should().Be(2); + } + + private static BreadcrumbBridgeRouter RouterOver( + Mock provider, + Mock host + ) + { + return new BreadcrumbBridgeRouter( + provider.Object, + host.Object, + new BreadcrumbMessageCodec(), + new BreadcrumbHtmlRenderer(), + new BreadcrumbOutboundQueue(host.Object) + ); + } + + private static Mock StrictHost(List documents) + { + var host = new Mock(MockBehavior.Strict); + host.SetupGet(h => h.IsCoreInitialized).Returns(true); + host.Setup(h => h.NavigateToString(It.IsAny())) + .Callback(html => documents.Add(html)); + host.Setup(h => h.PostMessageJson(It.IsAny())); + return host; + } + + private static void SetupChain( + Mock provider, + string hierarchyPath, + FolderTreeNodeKey key, + IReadOnlyList chain + ) + { + provider + .Setup(p => p.ResolveLeafKeyAsync(hierarchyPath, It.IsAny())) + .ReturnsAsync(key); + provider + .Setup(p => p.GetAncestorChainAsync(key, It.IsAny())) + .ReturnsAsync(chain); + } + + /// + /// An ancestor chain that already begins below the archive root, which is the shape the + /// provider returns once the #799 trim is in place. + /// + private static IReadOnlyList TwoSegmentChain( + string parentPath, + string parentName, + string leafPath, + string leafName + ) + { + return new[] + { + Segment(parentPath, parentName, true), + Segment(leafPath, leafName, false), + }; + } + + private static FolderBreadcrumbSegment Segment(string path, string name, bool hasChildren) + { + return new FolderBreadcrumbSegment(Key(path), name, path, hasChildren); + } + + private static FolderTreeNodeKey Key(string path) + { + return new FolderTreeNodeKey("archive-store", path, path); + } + + private static int Occurrences(string haystack, string needle) + { + int count = 0; + int index = haystack.IndexOf(needle, StringComparison.Ordinal); + while (index >= 0) + { + count++; + index = haystack.IndexOf(needle, index + needle.Length, StringComparison.Ordinal); + } + + return count; + } + } +} diff --git a/QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs b/QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs index d3c80da5f..87189bdf3 100644 --- a/QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs +++ b/QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs @@ -208,6 +208,12 @@ public void AssignFolderComboBox_WhenArchiveRootedPredeterminedFolder_Preselects /// path that does not start with the root, a path equal to the root plus a separator with /// nothing after it, and a case-differing root are each pinned, so the helper cannot be /// simplified into something that mangles a non-archive path. + /// + /// Retargeted by issue #799 AC4: an EMPTY archive root is now the identity projection. + /// The former behaviour formed an archive prefix of a single separator and stripped it, + /// which produced a value that was neither a valid full path nor a valid archive-relative + /// stem. The other five boundary cases are unchanged, because the shared projection + /// reproduces each of them exactly. /// [TestMethod] public void ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection() @@ -220,9 +226,9 @@ public void ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProject .ProjectPredeterminedFolder(@"\\Archive\Projects\Active", string.Empty) .Should() .Be( - @"\Archive\Projects\Active", - "a non-null globals with an EMPTY archive root gives FolderPredictor an " - + "archivePrefix of one separator, which it strips" + @"\\Archive\Projects\Active", + "AC4 of issue #799 removed the empty-root strip, so an empty archive root " + + "is now the identity projection" ); QfcItemController .ProjectPredeterminedFolder(null, @"\\Archive") @@ -243,12 +249,12 @@ public void ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProject } /// - /// Issue #678, remediation R2. The boundary case the projection previously got wrong: a - /// non-null globals whose ArchiveRootPath is EMPTY, with a leading-separator - /// suggestion path. FolderPredictor.ProjectSuggestionPath guards only on - /// _globals is null and then forms ArchiveRootPath + "\\" unconditionally, so - /// in this state its prefix is a single separator and its FolderArray entries ARE - /// stripped. The carried PredeterminedFolder must be projected the same way, or + /// Issue #678, remediation R2, re-derived against the issue #799 AC4 behaviour: a non-null + /// globals whose ArchiveRootPath is EMPTY, with a leading-separator suggestion path. + /// The shared projection now leaves BOTH the FolderArray entries and the carried + /// PredeterminedFolder unchanged in that state, so the two must agree on the + /// unstripped value. The invariant under test is unchanged and is the one that matters: + /// the carried value must be projected exactly as the array entries are, or /// FolderContains misses and the selection falls back to the index-1 entry — the /// exact AC12 defect the change set out to close. /// @@ -261,7 +267,10 @@ public void AssignFolderComboBox_WhenEmptyArchiveRootAndLeadingSeparator_Presele { // Arrange const string RawSuggestion = @"\Projects\Active"; - const string ProjectedSuggestion = @"Projects\Active"; + + // #799 AC4: an empty archive root is the identity projection, so the projected value + // and the raw value are the same string. + const string ProjectedSuggestion = RawSuggestion; var mock = new Mock(); mock.SetupGet(v => v.InvokeRequired).Returns(false); @@ -288,8 +297,8 @@ public void AssignFolderComboBox_WhenEmptyArchiveRootAndLeadingSeparator_Presele mock.Verify( v => v.SetFolderSelectedItem(ProjectedSuggestion), Times.Once(), - "an empty archive root still strips the leading separator in FolderPredictor, so " - + "the carried value must be stripped the same way to match" + "an empty archive root is the identity projection in FolderPredictor, so the " + + "carried value must be carried through the same way to match" ); mock.Verify( v => v.SetFolderSelectedIndex(It.IsAny()), diff --git a/QuickFiler.Test/QuickFiler.Test.csproj b/QuickFiler.Test/QuickFiler.Test.csproj index 8a3a30030..f0d479502 100644 --- a/QuickFiler.Test/QuickFiler.Test.csproj +++ b/QuickFiler.Test/QuickFiler.Test.csproj @@ -63,6 +63,7 @@ + diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs new file mode 100644 index 000000000..e89a70ef7 --- /dev/null +++ b/UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs @@ -0,0 +1,217 @@ +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UtilitiesCS.OutlookObjects.Folder; + +namespace UtilitiesCS.Test.OutlookObjects.Folder +{ + /// + /// Unit tests for and its single member + /// TryTrimBelowArchiveRoot, the ancestor-chain trim introduced by issue #799 (AC1, AC2). + /// Every chain is built from literals through the + /// four-argument constructor, so there is no snapshot, no provider, no COM, and no mock: the + /// unit under test is the trim itself and nothing else. + /// + [TestClass] + public sealed class ArchiveChainProjectionTests + { + private const string StorePath = "\\\\Mailbox - User"; + private const string ArchiveRoot = "\\\\Mailbox - User\\Archive"; + private const string ClientsPath = "\\\\Mailbox - User\\Archive\\Clients"; + private const string AcmePath = "\\\\Mailbox - User\\Archive\\Clients\\Acme"; + + /// + /// The ordinary case: the chain passes through the archive root, so the trim yields exactly + /// the segments after that node. Segment identity is preserved by reference, which proves + /// the trim is a projection over the existing chain rather than a rebuild that could drop a + /// key or a display name. + /// + [TestMethod] + public void TryTrimBelowArchiveRoot_ChainPassesThroughRoot_ReturnsSegmentsAfterTheRoot() + { + // Arrange + var chain = FullChain(); + + // Act + bool trimmedOk = ArchiveChainProjection.TryTrimBelowArchiveRoot( + chain, + ArchiveRoot, + out var trimmed + ); + + // Assert + trimmedOk.Should().BeTrue(); + trimmed.Should().HaveCount(2); + trimmed[0].Should().BeSameAs(chain[2], "segment identity is preserved by reference"); + trimmed[1].Should().BeSameAs(chain[3]); + } + + /// + /// A chain that never reaches the archive root is the AC2 diagnostic case: the trim reports + /// failure and yields nothing, so the caller can log once and fall back. + /// + [TestMethod] + public void TryTrimBelowArchiveRoot_ChainMissesTheRoot_ReturnsFalseAndEmptyOutput() + { + // Arrange + var chain = new List + { + Segment("inbox", StorePath + "\\Inbox", "Inbox"), + Segment("inbox-clients", StorePath + "\\Inbox\\Clients", "Clients"), + }; + + // Act + bool trimmedOk = ArchiveChainProjection.TryTrimBelowArchiveRoot( + chain, + ArchiveRoot, + out var trimmed + ); + + // Assert + trimmedOk.Should().BeFalse(); + trimmed.Should().BeEmpty(); + } + + /// + /// When the LEAF is the archive root there is nothing below it to render, so the trim + /// reports failure rather than returning an empty lineage that would render as a blank row. + /// + [TestMethod] + public void TryTrimBelowArchiveRoot_LeafIsTheRoot_ReturnsFalseAndEmptyOutput() + { + // Arrange + var chain = new List + { + Segment("store", StorePath, "Mailbox - User"), + Segment("archive", ArchiveRoot, "Archive"), + }; + + // Act + bool trimmedOk = ArchiveChainProjection.TryTrimBelowArchiveRoot( + chain, + ArchiveRoot, + out var trimmed + ); + + // Assert + trimmedOk.Should().BeFalse(); + trimmed.Should().BeEmpty(); + } + + /// An empty chain has no archive-root node and therefore reports failure. + [TestMethod] + public void TryTrimBelowArchiveRoot_EmptyChain_ReturnsFalseAndEmptyOutput() + { + // Arrange + var chain = new List(); + + // Act + bool trimmedOk = ArchiveChainProjection.TryTrimBelowArchiveRoot( + chain, + ArchiveRoot, + out var trimmed + ); + + // Assert + trimmedOk.Should().BeFalse(); + trimmed.Should().BeEmpty(); + } + + /// + /// A single-element chain that IS the archive root is the degenerate form of the + /// leaf-is-the-root case and must behave identically. + /// + [TestMethod] + public void TryTrimBelowArchiveRoot_SingleElementChainIsTheRoot_ReturnsFalse() + { + // Arrange + var chain = new List + { + Segment("archive", ArchiveRoot, "Archive"), + }; + + // Act + bool trimmedOk = ArchiveChainProjection.TryTrimBelowArchiveRoot( + chain, + ArchiveRoot, + out var trimmed + ); + + // Assert + trimmedOk.Should().BeFalse(); + trimmed.Should().BeEmpty(); + } + + /// A root supplied with a trailing separator trims identically. + [TestMethod] + public void TryTrimBelowArchiveRoot_RootWithTrailingSeparator_ReturnsSegmentsAfterTheRoot() + { + // Arrange + var chain = FullChain(); + + // Act + bool trimmedOk = ArchiveChainProjection.TryTrimBelowArchiveRoot( + chain, + ArchiveRoot + "\\", + out var trimmed + ); + + // Assert + trimmedOk.Should().BeTrue(); + trimmed.Should().HaveCount(2); + trimmed[0].FolderPath.Should().Be(ClientsPath); + } + + /// + /// The #614 false-prefix case at chain level: a lineage under a sibling folder named + /// Archive2 must not be treated as passing through the root named Archive. + /// + [TestMethod] + public void TryTrimBelowArchiveRoot_FalsePrefixSiblingArchive2_ReturnsFalse() + { + // Arrange + var chain = new List + { + Segment("archive2", StorePath + "\\Archive2", "Archive2"), + Segment("archive2-clients", StorePath + "\\Archive2\\Clients", "Clients"), + }; + + // Act + bool trimmedOk = ArchiveChainProjection.TryTrimBelowArchiveRoot( + chain, + ArchiveRoot, + out var trimmed + ); + + // Assert + trimmedOk.Should().BeFalse("the separator-boundary test rejects a false prefix"); + trimmed.Should().BeEmpty(); + } + + /// Store node, archive root, one intermediate folder, and the leaf. + private static List FullChain() + { + return new List + { + Segment("store", StorePath, "Mailbox - User"), + Segment("archive", ArchiveRoot, "Archive"), + Segment("clients", ClientsPath, "Clients"), + Segment("acme", AcmePath, "Acme"), + }; + } + + private static FolderBreadcrumbSegment Segment( + string entryId, + string folderPath, + string displayName + ) + { + return new FolderBreadcrumbSegment( + new FolderTreeNodeKey("store-a", entryId, folderPath), + displayName, + folderPath, + false + ); + } + } +} diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs new file mode 100644 index 000000000..9b7338a4f --- /dev/null +++ b/UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs @@ -0,0 +1,176 @@ +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UtilitiesCS.OutlookObjects.Folder; + +namespace UtilitiesCS.Test.OutlookObjects.Folder +{ + /// + /// Unit tests for and its single member + /// ToDisplayStem, the lenient display projection introduced by issue #799. + /// The projection yields the archive-relative stem + /// only when the path is strictly under the configured root, and returns its input unchanged in + /// every other case. The boundary cases pinned here are the #614 cases the strict + /// already enforces, restated at the display boundary because + /// the fallback there is the opposite one: show the caller's own text rather than nothing. + /// Pure assertions only: no mocks, no COM, no filesystem, and no temporary file. + /// + [TestClass] + public sealed class ArchiveStemProjectionTests + { + private const string ArchiveRoot = "\\\\Mailbox - User\\Archive"; + private const string UnderRoot = "\\\\Mailbox - User\\Archive\\Clients\\Acme"; + private const string ExpectedStem = "Clients\\Acme"; + + /// The ordinary case: a path strictly under the root projects to its stem. + [TestMethod] + public void ToDisplayStem_PathStrictlyUnderRoot_ReturnsArchiveRelativeStem() + { + // Arrange, Act + var projected = ArchiveStemProjection.ToDisplayStem(UnderRoot, ArchiveRoot); + + // Assert + projected.Should().Be(ExpectedStem); + } + + /// + /// A path EQUAL to the root is not projectable: the strict contract reports success with an + /// empty stem, and an empty display row is worse than the full path, so the input is + /// returned unchanged. + /// + [TestMethod] + public void ToDisplayStem_PathEqualsRoot_ReturnsInputUnchanged() + { + // Arrange, Act + var projected = ArchiveStemProjection.ToDisplayStem(ArchiveRoot, ArchiveRoot); + + // Assert + projected + .Should() + .Be(ArchiveRoot, "an empty display row is worse than the unprojected path"); + } + + /// + /// The #614 false-prefix case. A sibling folder named Archive2 tested against a root ending + /// in Archive yields the character 2 at the root's length, which is not a separator, so the + /// path is NOT under the root and is not projected. + /// + [TestMethod] + public void ToDisplayStem_FalsePrefixSiblingArchive2_ReturnsInputUnchanged() + { + // Arrange + const string sibling = "\\\\Mailbox - User\\Archive2\\Clients"; + + // Act + var projected = ArchiveStemProjection.ToDisplayStem(sibling, ArchiveRoot); + + // Assert + projected.Should().Be(sibling, "the separator-boundary test rejects a false prefix"); + } + + /// A root supplied with one trailing separator projects identically. + [TestMethod] + public void ToDisplayStem_RootWithOneTrailingSeparator_ReturnsArchiveRelativeStem() + { + // Arrange, Act + var projected = ArchiveStemProjection.ToDisplayStem(UnderRoot, ArchiveRoot + "\\"); + + // Assert + projected.Should().Be(ExpectedStem); + } + + /// A root supplied with two trailing separators projects identically. + [TestMethod] + public void ToDisplayStem_RootWithTwoTrailingSeparators_ReturnsArchiveRelativeStem() + { + // Arrange, Act + var projected = ArchiveStemProjection.ToDisplayStem(UnderRoot, ArchiveRoot + "\\\\"); + + // Assert + projected.Should().Be(ExpectedStem); + } + + /// + /// AC4 of issue #799: an empty archive root leaves the input unchanged. The previous + /// per-site logic stripped one leading separator in this case, which produced a path that + /// was neither a valid full path nor a valid archive-relative stem. + /// + [TestMethod] + public void ToDisplayStem_EmptyRoot_ReturnsInputUnchanged() + { + // Arrange, Act + var projected = ArchiveStemProjection.ToDisplayStem(UnderRoot, string.Empty); + + // Assert + projected + .Should() + .Be(UnderRoot, "#799 AC4 removed the empty-root one-separator strip"); + } + + /// A whitespace-only root is treated exactly as an empty root. + [TestMethod] + public void ToDisplayStem_WhitespaceOnlyRoot_ReturnsInputUnchanged() + { + // Arrange, Act + var projected = ArchiveStemProjection.ToDisplayStem(UnderRoot, " "); + + // Assert + projected.Should().Be(UnderRoot); + } + + /// A null path is returned unchanged rather than throwing. + [TestMethod] + public void ToDisplayStem_NullPath_ReturnsNull() + { + // Arrange, Act + var projected = ArchiveStemProjection.ToDisplayStem(null, ArchiveRoot); + + // Assert + projected.Should().BeNull(); + } + + /// An empty path is returned unchanged rather than projected. + [TestMethod] + public void ToDisplayStem_EmptyPath_ReturnsInputUnchanged() + { + // Arrange, Act + var projected = ArchiveStemProjection.ToDisplayStem(string.Empty, ArchiveRoot); + + // Assert + projected.Should().Be(string.Empty); + } + + /// + /// Forward-slash separators on both parameters project exactly as backslash separators do, + /// because the underlying contract treats both characters as separators. + /// + [TestMethod] + public void ToDisplayStem_ForwardSlashSeparators_ReturnsArchiveRelativeStem() + { + // Arrange + const string root = "//Mailbox - User/Archive"; + const string path = "//Mailbox - User/Archive/Clients/Acme"; + + // Act + var projected = ArchiveStemProjection.ToDisplayStem(path, root); + + // Assert + projected.Should().Be("Clients/Acme"); + } + + /// + /// The prefix comparison is ordinal case-insensitive, so a mixed-case root still projects. + /// + [TestMethod] + public void ToDisplayStem_MixedCaseRoot_ReturnsArchiveRelativeStem() + { + // Arrange + const string mixedCaseRoot = "\\\\mailbox - USER\\aRcHiVe"; + + // Act + var projected = ArchiveStemProjection.ToDisplayStem(UnderRoot, mixedCaseRoot); + + // Assert + projected.Should().Be(ExpectedStem); + } + } +} diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs new file mode 100644 index 000000000..34576beca --- /dev/null +++ b/UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using UtilitiesCS.ReusableTypeClasses.SerializableNew.Concurrent.Observable; +using Outlook = Microsoft.Office.Interop.Outlook; +using OutlookFolder = Microsoft.Office.Interop.Outlook.Folder; +using OutlookFolders = Microsoft.Office.Interop.Outlook.Folders; + +namespace UtilitiesCS.Test.OutlookObjects.Folder +{ + /// + /// Tests for AC5 of issue #799: recent-folder entries pass through the same archive-stem + /// display projection as suggestion entries before they are displayed. Today an + /// archive-rooted recent entry is appended verbatim, so a recent selection renders as a full + /// store path beside suggestions that render as archive-relative stems. + /// + /// Every test seeds the recents list with one archive-rooted entry AND one already-relative + /// entry, so the projection is observable and the identity case is pinned by the same fixture. + /// The mocked-Outlook harness mirrors FolderRowTests; no live Outlook process, COM + /// server, or temporary file is used. + /// + /// + [TestClass] + public sealed class FolderPredictorRecentsProjectionTests + { + private const string ArchiveRootPath = "\\\\ArchiveRoot"; + private const string RootedRecent = "\\\\ArchiveRoot\\Recent\\One"; + private const string RootedRecentStem = "Recent\\One"; + private const string RelativeRecent = "Recent\\Two"; + private const string OutOfRootRecent = "\\\\OtherRoot\\Recent\\Three"; + private const string RecentsSeparator = "======= RECENT SELECTIONS ========"; + + /// + /// AC5 on the legacy string surface: the archive-rooted recent entry is projected to its + /// archive-relative stem, and the already-relative entry is left alone. + /// + [TestMethod] + public void FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem() + { + // Arrange + var predictor = PredictorWithRecents(RootedRecent, RelativeRecent); + + // Act + var folderArray = predictor.FolderArray; + + // Assert + folderArray.Should().Equal(RecentsSeparator, RootedRecentStem, RelativeRecent); + } + + /// + /// AC5 on the row-model surface: FolderRowArray projects the archive-rooted recent + /// entry identically, and still tags both entries as recent selections. + /// + [TestMethod] + public void FolderRowArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem() + { + // Arrange + var predictor = PredictorWithRecents(RootedRecent, RelativeRecent); + + // Act + var rows = predictor.FolderRowArray; + + // Assert + rows.Select(r => r.Text) + .Should() + .Equal(RecentsSeparator, RootedRecentStem, RelativeRecent); + rows.Where(r => r.Kind == UtilitiesCS.FolderRowKind.Recent) + .Select(r => r.Text) + .Should() + .Equal(RootedRecentStem, RelativeRecent); + } + + /// + /// The text-parity contract documented on FolderRowArray is currently unasserted for + /// recents. Projecting one surface without the other would break it silently, so parity is + /// pinned here as its own criterion rather than as a side effect of the two tests above. + /// + [TestMethod] + public void FolderRowArray_AndFolderArray_AgreeOnRecentTextAfterProjection() + { + // Arrange + var predictor = PredictorWithRecents(RootedRecent, RelativeRecent); + + // Act + var folderArray = predictor.FolderArray; + var rows = predictor.FolderRowArray; + + // Assert + rows.Select(r => r.Text) + .Should() + .Equal(folderArray, "the row model mirrors FolderArray byte for byte"); + } + + /// + /// The projection is lenient: a recent entry that is NOT under the archive root is left + /// exactly as stored, because there is no stem to show and the full path is the only + /// meaningful text. + /// + [TestMethod] + public void FolderArray_OutOfRootRecentEntry_IsLeftUnchanged() + { + // Arrange + var predictor = PredictorWithRecents(RootedRecent, RelativeRecent, OutOfRootRecent); + + // Act + var folderArray = predictor.FolderArray; + + // Assert + folderArray + .Should() + .Equal(RecentsSeparator, RootedRecentStem, RelativeRecent, OutOfRootRecent); + } + + private static UtilitiesCS.FolderPredictor PredictorWithRecents(params string[] recents) + { + var archiveRoot = CreateFolder( + ArchiveRootPath, + new Dictionary() + ); + var app = CreateApplication( + new Dictionary { ["ArchiveRoot"] = archiveRoot.Object } + ); + var globals = CreateGlobals(app, archiveRoot.Object, recents); + return new UtilitiesCS.FolderPredictor(globals.Object); + } + + // ---- Mocked-Outlook harness (mirrors FolderRowTests) ---- + + private static Mock CreateGlobals( + Mock app, + OutlookFolder rootFolder, + IEnumerable recents + ) + { + var autoFile = new Mock(); + autoFile.SetupGet(x => x.RecentsList).Returns(new SloLinkedList(recents)); + + var olObjects = new Mock(); + olObjects.SetupGet(x => x.App).Returns(app.Object); + olObjects.SetupGet(x => x.ArchiveRootPath).Returns(rootFolder.FolderPath); + olObjects.SetupGet(x => x.Root).Returns(rootFolder); + + var globals = new Mock(); + globals.SetupGet(x => x.AF).Returns(autoFile.Object); + globals.SetupGet(x => x.Ol).Returns(olObjects.Object); + return globals; + } + + private static Mock CreateApplication( + IDictionary rootFolders + ) + { + var app = new Mock(); + var nameSpace = new Mock(); + nameSpace.SetupGet(x => x.Folders).Returns(CreateFoldersCollection(rootFolders).Object); + app.SetupGet(x => x.Session).Returns(nameSpace.Object); + return app; + } + + private static Mock CreateFolder( + string folderPath, + IDictionary childFolders + ) + { + var folder = new Mock(); + folder.SetupGet(x => x.Name).Returns(GetLeafName(folderPath)); + folder.SetupGet(x => x.FolderPath).Returns(folderPath); + folder + .SetupGet(x => x.Folders) + .Returns( + CreateFoldersCollection( + childFolders ?? new Dictionary() + ).Object + ); + return folder; + } + + private static Mock CreateFoldersCollection( + IDictionary foldersByName + ) + { + var folders = new Mock(); + var enumerableItems = + foldersByName?.Values?.ToArray() ?? Array.Empty(); + var collection = new ArrayList(enumerableItems); + + folders + .Setup(x => x[It.IsAny()]) + .Returns(key => + { + if ( + key is string name + && foldersByName.TryGetValue(name, out OutlookFolder folder) + ) + { + return folder; + } + return null; + }); + folders.Setup(x => x.GetEnumerator()).Returns(() => collection.GetEnumerator()); + return folders; + } + + private static string GetLeafName(string folderPath) + { + return folderPath.Split('\\').Last(segment => !string.IsNullOrWhiteSpace(segment)); + } + } +} diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs index 20f76d3e4..e62db7a6a 100644 --- a/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs +++ b/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs @@ -70,11 +70,12 @@ public sealed class OutlookFolderHierarchyProviderTests ); [TestMethod] - public async Task GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments() + public async Task GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath() { - // Arrange + // Arrange: configured as production configures it, with a root accessor (#799 AC1). var provider = new OutlookFolderHierarchyProvider( - ServiceReturning(BuildSnapshot()).Object + ServiceReturning(BuildSnapshot()).Object, + () => "\\Root" ); // Act @@ -84,7 +85,7 @@ public async Task GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments() chain .Select(s => s.FolderPath) .Should() - .Equal("\\Root", "\\Root\\Clients", "\\Root\\Clients\\Acme"); + .Equal("\\Root\\Clients", "\\Root\\Clients\\Acme"); chain.Last().Key.Should().Be(AcmeKey); chain.Last().HasChildren.Should().BeFalse(); chain.First().HasChildren.Should().BeTrue(); diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs new file mode 100644 index 000000000..d9461ac0e --- /dev/null +++ b/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using UtilitiesCS.OutlookObjects.Folder; + +namespace UtilitiesCS.Test.OutlookObjects.Folder +{ + /// + /// Unit tests for the issue #799 seams on : the + /// archive-root chain trim (AC1, AC2) and the label-absence classification (AC7). The folder + /// tree is a hand-built returned by a Moq + /// , and diagnostics are observed through the provider's + /// injected error sink rather than by attaching a log4net appender, so no test mutates the + /// process-global logger repository. No live Outlook process, COM, or temporary file is used. + /// + [TestClass] + public sealed class OutlookFolderHierarchyProviderTrimTests + { + private const string StorePath = "\\\\Mailbox - User"; + private const string ArchivePath = "\\\\Mailbox - User\\Archive"; + private const string ProjectsPath = "\\\\Mailbox - User\\Archive\\Projects"; + private const string AlphaPath = "\\\\Mailbox - User\\Archive\\Projects\\Alpha"; + private const string InboxPath = "\\\\Mailbox - User\\Inbox"; + private const string InboxProjectsPath = "\\\\Mailbox - User\\Inbox\\Projects"; + private const string InboxAlphaPath = "\\\\Mailbox - User\\Inbox\\Projects\\Alpha"; + private const string PresentedStem = "Projects\\Alpha"; + private const string MissingStem = "Missing\\Folder"; + + private static readonly FolderTreeNodeKey StoreKey = Key("store", StorePath); + private static readonly FolderTreeNodeKey ArchiveKey = Key("archive", ArchivePath); + private static readonly FolderTreeNodeKey ProjectsKey = Key("projects", ProjectsPath); + private static readonly FolderTreeNodeKey AlphaKey = Key("alpha", AlphaPath); + private static readonly FolderTreeNodeKey InboxKey = Key("inbox", InboxPath); + private static readonly FolderTreeNodeKey InboxProjectsKey = Key( + "inbox-projects", + InboxProjectsPath + ); + private static readonly FolderTreeNodeKey InboxAlphaKey = Key( + "inbox-alpha", + InboxAlphaPath + ); + + /// + /// AC1: with a root accessor configured, the presented lineage begins at the first segment + /// BELOW the archive root. Neither the store node nor the archive-root node may appear. + /// + [TestMethod] + public async Task GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot() + { + // Arrange + var provider = ProviderOver(ArchiveSnapshot(), () => ArchivePath); + + // Act + var chain = await provider.GetAncestorChainAsync(AlphaKey, CancellationToken.None); + + // Assert + chain.Select(s => s.FolderPath).Should().Equal(ProjectsPath, AlphaPath); + chain + .Select(s => s.FolderPath) + .Should() + .NotContain(StorePath, "the mailbox segment is never presented"); + chain + .Select(s => s.FolderPath) + .Should() + .NotContain(ArchivePath, "the Archive segment is never presented"); + } + + /// + /// Constructing the provider WITHOUT a root accessor is the effective off switch: the chain + /// is returned exactly as the snapshot walk produced it. + /// + [TestMethod] + public async Task GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain() + { + // Arrange + var provider = new OutlookFolderHierarchyProvider( + ServiceReturning(ArchiveSnapshot()).Object + ); + + // Act + var chain = await provider.GetAncestorChainAsync(AlphaKey, CancellationToken.None); + + // Assert + chain + .Select(s => s.FolderPath) + .Should() + .Equal(StorePath, ArchivePath, ProjectsPath, AlphaPath); + } + + /// + /// AC2: a resolved chain that does not pass through the archive root is logged exactly once + /// and yields an empty segment list, which routes each surface into its existing + /// single-segment fallback. The emitted text must name the archive root so the failure is + /// diagnosable from the log alone. + /// + [TestMethod] + public async Task GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty() + { + // Arrange + var errors = new List(); + var provider = ProviderOver(ArchiveSnapshot(), () => StorePath + "\\Elsewhere"); + provider.ErrorSink = message => errors.Add(message); + + // Act + var chain = await provider.GetAncestorChainAsync(AlphaKey, CancellationToken.None); + + // Assert + chain.Should().BeEmpty("the caller falls back to single-segment rendering"); + errors.Should().ContainSingle("the AC2 diagnostic is emitted exactly once"); + errors[0].Should().Contain(StorePath + "\\Elsewhere"); + } + + /// + /// When the LEAF is the archive root itself there is nothing below it to render, so the + /// same AC2 diagnostic and empty result apply. + /// + [TestMethod] + public async Task GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty() + { + // Arrange + var errors = new List(); + var provider = ProviderOver(ArchiveSnapshot(), () => ArchivePath); + provider.ErrorSink = message => errors.Add(message); + + // Act + var chain = await provider.GetAncestorChainAsync(ArchiveKey, CancellationToken.None); + + // Assert + chain.Should().BeEmpty(); + errors.Should().ContainSingle(); + } + + /// + /// The accessor is a delegate precisely because the underlying archive-root property throws + /// when the root is unresolvable. A throwing accessor means "no trim configured" and must + /// not propagate, because two of the three construction sites are outside any try block. + /// + [TestMethod] + public async Task GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain() + { + // Arrange + var provider = ProviderOver( + ArchiveSnapshot(), + () => throw new InvalidOperationException("archive root unresolvable") + ); + + // Act + var chain = await provider.GetAncestorChainAsync(AlphaKey, CancellationToken.None); + + // Assert + chain + .Select(s => s.FolderPath) + .Should() + .Equal(StorePath, ArchivePath, ProjectsPath, AlphaPath); + } + + /// + /// AC7 logging half: an unresolvable label is reported once per label per provider + /// instance, not once per render, and the label is classified as absent. + /// + [TestMethod] + public async Task ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence() + { + // Arrange + var errors = new List(); + var provider = ProviderOver(ArchiveSnapshot(), () => ArchivePath); + provider.ErrorSink = message => errors.Add(message); + + // Act + await provider.ResolveLeafKeyAsync(MissingStem, CancellationToken.None); + await provider.ResolveLeafKeyAsync(MissingStem, CancellationToken.None); + + // Assert + errors.Should().ContainSingle("the gate is once per label per session, not per render"); + provider.IsAbsentLabel(MissingStem).Should().BeTrue(); + } + + /// + /// Decision D-B restricts the AC7 suppression signal to the ZERO-candidate cause. An + /// ambiguous label is still logged, but it is not absent: the folder exists, and more than + /// one candidate matched. + /// + [TestMethod] + public async Task ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence() + { + // Arrange + var errors = new List(); + var provider = ProviderOver(DecoySnapshot(), () => ArchivePath); + provider.ErrorSink = message => errors.Add(message); + + // Act + var resolved = await provider.ResolveLeafKeyAsync( + PresentedStem, + CancellationToken.None + ); + + // Assert + resolved.Should().BeNull("an ambiguous stem is never resolved to either candidate"); + errors.Should().ContainSingle(); + provider + .IsAbsentLabel(PresentedStem) + .Should() + .BeFalse("ambiguity is not absence; the folder does exist"); + } + + /// + /// The absence signal must RESET, or a label that becomes resolvable after a snapshot + /// refresh would stay suppressed for the life of the viewer. The service returns a snapshot + /// missing the leaf on the first call and the complete snapshot on the second. + /// + [TestMethod] + public async Task ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport() + { + // Arrange + var service = new Mock(); + service + .SetupSequence(s => + s.GetSnapshotAsync(It.IsAny(), It.IsAny()) + ) + .ReturnsAsync(SnapshotWithoutLeaf()) + .ReturnsAsync(ArchiveSnapshot()); + var provider = new OutlookFolderHierarchyProvider(service.Object, () => ArchivePath); + + // Act + await provider.ResolveLeafKeyAsync(PresentedStem, CancellationToken.None); + bool absentBefore = provider.IsAbsentLabel(PresentedStem); + var resolved = await provider.ResolveLeafKeyAsync( + PresentedStem, + CancellationToken.None + ); + + // Assert + absentBefore.Should().BeTrue("the first snapshot had no node for the label"); + resolved.Should().Be(AlphaKey); + provider + .IsAbsentLabel(PresentedStem) + .Should() + .BeFalse("the label resolved, so the suppression signal must clear"); + } + + private static OutlookFolderHierarchyProvider ProviderOver( + FolderTreeSnapshot snapshot, + Func archiveRootAccessor + ) + { + return new OutlookFolderHierarchyProvider( + ServiceReturning(snapshot).Object, + archiveRootAccessor + ); + } + + private static Mock ServiceReturning(FolderTreeSnapshot snapshot) + { + var service = new Mock(); + service + .Setup(s => + s.GetSnapshotAsync(It.IsAny(), It.IsAny()) + ) + .ReturnsAsync(snapshot); + return service; + } + + /// Store, Archive, Projects, Alpha: a store-rooted three-level Archive chain. + private static FolderTreeSnapshot ArchiveSnapshot() + { + return new FolderTreeSnapshot( + new[] { StoreKey }, + new[] + { + Node(StoreKey, "Mailbox - User", null, ArchiveKey), + Node(ArchiveKey, "Archive", StoreKey, ProjectsKey), + Node(ProjectsKey, "Projects", ArchiveKey, AlphaKey), + Node(AlphaKey, "Alpha", ProjectsKey), + } + ); + } + + /// The Archive chain with the Alpha leaf absent, so the stem has zero candidates. + private static FolderTreeSnapshot SnapshotWithoutLeaf() + { + return new FolderTreeSnapshot( + new[] { StoreKey }, + new[] + { + Node(StoreKey, "Mailbox - User", null, ArchiveKey), + Node(ArchiveKey, "Archive", StoreKey, ProjectsKey), + Node(ProjectsKey, "Projects", ArchiveKey), + } + ); + } + + /// + /// The Archive chain plus an Inbox chain whose leaf shares the last two segments, so a + /// suffix match on the presented stem is ambiguous rather than absent. + /// + private static FolderTreeSnapshot DecoySnapshot() + { + return new FolderTreeSnapshot( + new[] { StoreKey }, + new[] + { + Node(StoreKey, "Mailbox - User", null, ArchiveKey, InboxKey), + Node(ArchiveKey, "Archive", StoreKey, ProjectsKey), + Node(ProjectsKey, "Projects", ArchiveKey, AlphaKey), + Node(AlphaKey, "Alpha", ProjectsKey), + Node(InboxKey, "Inbox", StoreKey, InboxProjectsKey), + Node(InboxProjectsKey, "Projects", InboxKey, InboxAlphaKey), + Node(InboxAlphaKey, "Alpha", InboxProjectsKey), + } + ); + } + + private static FolderTreeNodeKey Key(string entryId, string folderPath) + { + return new FolderTreeNodeKey("store-a", entryId, folderPath); + } + + private static FolderTreeSnapshotNode Node( + FolderTreeNodeKey key, + string displayName, + FolderTreeNodeKey parentKey, + params FolderTreeNodeKey[] childKeys + ) + { + return new FolderTreeSnapshotNode( + key, + displayName, + key.StoreId, + key.EntryId, + parentKey, + key.FolderPath, + displayName, + childKeys, + false, + string.Empty + ); + } + } +} diff --git a/UtilitiesCS.Test/UtilitiesCS.Test.csproj b/UtilitiesCS.Test/UtilitiesCS.Test.csproj index a4c2e8c0d..9702d6a98 100644 --- a/UtilitiesCS.Test/UtilitiesCS.Test.csproj +++ b/UtilitiesCS.Test/UtilitiesCS.Test.csproj @@ -304,6 +304,10 @@ + + + + diff --git a/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs b/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs new file mode 100644 index 000000000..0421b2691 --- /dev/null +++ b/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs @@ -0,0 +1,54 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace UtilitiesCS.OutlookObjects.Folder +{ + /// + /// Trims a breadcrumb ancestor chain so that only the lineage BELOW the configured Outlook + /// archive root is presented (#799 AC1 and AC2). + /// + /// Pure by construction: no filesystem, network, COM, logging, or environment access. Segment + /// instances are passed through by reference; no segment is rebuilt or reordered. + /// + /// + public static class ArchiveChainProjection + { + /// + /// Finds the archive-root node in and yields the remainder of the + /// chain that follows it. + /// + /// The archive-root node is the FIRST chain index whose segment + /// is the root itself, detected as + /// + /// returning true with an EMPTY stem, which is exactly the path-equals-root case. The + /// method returns the segments after that index. + /// + /// + /// It returns false when no such index exists, and also when that index is the LAST + /// element: the leaf is then the archive root itself and there is nothing below it to + /// render. It also returns false for a null or empty chain and for a null, empty, or + /// whitespace-only root. + /// + /// + /// The root-to-leaf ancestor chain. Null returns false. + /// + /// The configured archive root. Null, empty, and whitespace-only roots return false. + /// + /// + /// The segments below the archive root on success; an empty list on every failing path. + /// + /// True when a proper lineage below the archive root exists; otherwise false. + public static bool TryTrimBelowArchiveRoot( + IReadOnlyList? chain, + string? archiveRoot, + out IReadOnlyList trimmed + ) + { + trimmed = Array.Empty(); + throw new NotImplementedException( + "Issue #799: the chain trim body is supplied by [P2-T2]." + ); + } + } +} diff --git a/UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs b/UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs new file mode 100644 index 000000000..3a7f064c3 --- /dev/null +++ b/UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs @@ -0,0 +1,48 @@ +#nullable enable +using System; + +namespace UtilitiesCS.OutlookObjects.Folder +{ + /// + /// Lenient DISPLAY projection of a full Outlook folder path onto its archive-relative stem. + /// + /// The projection returns the archive-relative stem when, and only when, the path is strictly + /// under the configured archive root, and returns the input UNCHANGED in every other case: + /// a path equal to the root, a path outside the root, a null or empty root, and a + /// whitespace-only root. That last case is the behaviour change AC4 of issue #799 requires; + /// the previous per-site logic stripped one leading separator when the root was empty. + /// + /// + /// This is a separate type rather than an additional overload on + /// because that contract is a hard boundary: it yields an + /// empty string on failure and never passes its input through, which is precisely the + /// invariant #614 created it to enforce. Every display site needs the opposite fallback — + /// show the caller's own text rather than nothing — so adding a lenient overload beside the + /// strict one would blur the boundary the contract exists to defend. + /// + /// + /// Pure by construction: no filesystem, network, COM, logging, or environment access. + /// + /// + public static class ArchiveStemProjection + { + /// + /// Projects onto its archive-relative stem for display. + /// + /// The candidate full Outlook path. Null is returned unchanged. + /// + /// The configured archive root. Null, empty, and whitespace-only roots disable the + /// projection and the input is returned unchanged. + /// + /// + /// The archive-relative stem when the path is strictly under the root; otherwise + /// unchanged. + /// + public static string? ToDisplayStem(string? folderPath, string? archiveRoot) + { + throw new NotImplementedException( + "Issue #799: the display projection body is supplied by [P2-T1]." + ); + } + } +} diff --git a/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs b/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs index 6a867ec2e..b43430b32 100644 --- a/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs +++ b/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs @@ -7,12 +7,39 @@ namespace UtilitiesCS.OutlookObjects.Folder { + /// + /// Reports whether a folder label was classified as ABSENT from the folder snapshot, meaning + /// resolution found ZERO candidate nodes for it (#799 AC7). An ambiguous label, for which + /// resolution found more than one candidate, is deliberately NOT absent. + /// + /// This is a separate, small interface rather than a fourth member on + /// for two reasons. net48 has no default interface + /// members, so a fourth member would break every implementer. Decisively, every breadcrumb + /// router test constructs a strict Mock<IFolderHierarchyProvider>, which would + /// throw the first time production called an un-set-up new member; because a strict mock is + /// simply not an , the consuming cast yields null and + /// the AC7 suppression stays inert in every existing router test. + /// + /// + public interface IFolderLabelAbsenceReport + { + /// + /// Reports whether was classified as absent from the + /// snapshot by the most recent resolution attempt against this instance. + /// + /// The presented folder path or label. + /// True when the label resolved to zero candidates; otherwise false. + bool IsAbsentLabel(string folderPath); + } + /// /// Host-neutral facade over that projects the cached /// into breadcrumb segments. Adds no COM code and is not /// coverage-exempt; the live Outlook query stays isolated behind the injected service interface. /// - public sealed class OutlookFolderHierarchyProvider : IFolderHierarchyProvider + public sealed class OutlookFolderHierarchyProvider + : IFolderHierarchyProvider, + IFolderLabelAbsenceReport { private static readonly log4net.ILog logger = log4net.LogManager.GetLogger( System.Reflection.MethodBase.GetCurrentMethod().DeclaringType @@ -24,12 +51,38 @@ public sealed class OutlookFolderHierarchyProvider : IFolderHierarchyProvider /// Creates a provider over the supplied folder-tree service. /// /// The cached snapshot service. Required. + /// + /// Optional lazy accessor for the configured Outlook archive root, used to trim the + /// ancestor chain to the lineage below that root (#799 AC1, AC2). It is a delegate rather + /// than a value because the underlying archive-root property throws when the root is + /// unresolvable, and reading it eagerly at construction would create a new throw site at + /// every construction site. A null accessor is the effective off switch and leaves the + /// chain untrimmed. + /// /// is null. - public OutlookFolderHierarchyProvider(IOutlookFolderTreeService treeService) + public OutlookFolderHierarchyProvider( + IOutlookFolderTreeService treeService, + System.Func? archiveRootAccessor = null + ) { _treeService = treeService ?? throw new ArgumentNullException(nameof(treeService)); + ArchiveRootAccessor = archiveRootAccessor; } + /// + /// The lazy archive-root accessor supplied at construction, or null when no trim is + /// configured. Stored as a get-only auto-property rather than a private readonly field so + /// that the seam-only intermediate state raises no CS0414 assigned-but-never-read warning. + /// + internal System.Func? ArchiveRootAccessor { get; } + + /// + /// Injected diagnostic sink that tests observe INSTEAD of attaching a log4net appender, so + /// no test mutates the process-global logger repository. Production leaves it null and the + /// provider logs through its own log4net logger only. + /// + internal System.Action? ErrorSink { get; set; } + /// public async Task> GetAncestorChainAsync( FolderTreeNodeKey leafKey, @@ -79,6 +132,12 @@ CancellationToken cancellationToken return ResolveByUniqueSuffix(snapshot, folderPath); } + /// + public bool IsAbsentLabel(string folderPath) => + throw new NotImplementedException( + "Issue #799: the absence report body is supplied by [P2-T6]." + ); + /// /// Second resolution pass for a relative stem such as Projects\Alpha, which the /// QuickFiler surface presents in place of a store-qualified path. Accepts a node whose diff --git a/UtilitiesCS/UtilitiesCS.csproj b/UtilitiesCS/UtilitiesCS.csproj index 0250500d9..75b0d7294 100644 --- a/UtilitiesCS/UtilitiesCS.csproj +++ b/UtilitiesCS/UtilitiesCS.csproj @@ -621,6 +621,8 @@ + + diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t15-test-build.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t15-test-build.md new file mode 100644 index 000000000..cf0c2be40 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t15-test-build.md @@ -0,0 +1,54 @@ +# [P1-T15] Test build — the new and retargeted Phase 1 tests compile + +Timestamp: 2026-09-07T07-13 + +Command: `msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"` + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +## Output Summary + +MSBuild summary tail: + +``` + 0 Warning(s) + 0 Error(s) +``` + +Build succeeded with 0 warnings and 0 errors, proving every new test compiles against the Phase 1 +seams. The five test files added or retargeted by [P1-T6] through [P1-T14] are in the build via the +[P1-T10] and [P1-T12] Compile Include entries. + +Files proved to compile by this run: + +- UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs (new) +- UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs (new) +- UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs (new) +- UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs (new) +- QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs (new) +- UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs (retargeted) +- QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs (retargeted) + +## Language-version observation + +Zero errors on QuickFiler.Test also confirms the [P1-T11] C# 7.3 authoring constraint was honoured. +That project declares no `LangVersion` element and targets v4.8.1, so it compiles at the 7.3 +default; a construct mirrored from a UtilitiesCS.Test file would have surfaced here as CS8370. + +## Command form (R10) + +`/t:Build` with no `/p:` gate switches. This build exists to produce test assemblies, not to run +gates. R10 reserves `/t:Rebuild` with the gate switches for [P3-T3] and [P3-T4]. + +## Environment note + +MSBuild is not on this machine's PATH. The invoking shell prepended the Visual Studio 18 Community +MSBuild `Current\Bin\amd64` directory before invoking the command. The command text itself is +character-for-character the form this task specifies. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. The raw MSBuild +log was written outside the repository to a session scratch location and is not committed. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t16-ut-fail.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t16-ut-fail.md new file mode 100644 index 000000000..a24f96487 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t16-ut-fail.md @@ -0,0 +1,108 @@ +# [P1-T16] [expect-fail] UtilitiesCS.Test fail-before run + +Timestamp: 2026-09-07T07-15 + +Command: `vstest.console.exe UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation /Logger:trx /ResultsDirectory:TestResults\799-p1-t16 /Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None /TestCaseFilter:FullyQualifiedName~ArchiveStemProjectionTests|FullyQualifiedName~ArchiveChainProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTrimTests|FullyQualifiedName~FolderPredictorRecentsProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTests` + +EXIT_CODE: 1 + +ExpectedExitCode: 1 + +## Output Summary + +`Test Run Failed.` — total 43, passed 15, failed 28. The failing set is the intended Phase 1 red +set: 28 tests fail before the Phase 2 production change, and no test fails for an unrelated reason. + +## TRX read + +TRX file (name reduced per R3): `__2026-09-07_07_15_03_net481.trx`, the most recently +modified TRX under the results directory for this task. Counter values are read from the +`ResultSummary/Counters` element. No raw TRX content is pasted (R3). + +- TOTAL: 43 +- PASSED: 15 +- FAILED: 28 + +## Suite selection (R13) + +This is a SINGLE-assembly run driven by an explicit `FullyQualifiedName~` class filter over five +UtilitiesCS.Test classes. None of the four environmentally-hanging shell-icon classes +(`HelperClasses.ShellUtilities_Tests`, `HelperClasses.ShellUtilitiesStatic_Tests`, +`HelperClasses.SysImageListHelperTests`, `EmailIntelligence.OSBrowser_Tests`) is selected by that +filter, so the R13 exclusion clauses are unnecessary here and the reduced denominator is the 43 +tests of the five named classes. The filter contains no `&` clause, so the `&`-binds-tighter-than-`|` +precedence hazard does not apply. + +## Failing tests, by fully qualified name and cause + +`SEAM-BLOCKED` means the test fails because a Phase 1 declaration seam throws +`NotImplementedException`; `NEW` means the test is new and fails because the production behaviour is +not yet changed; `RETARGETED` means an existing test was retargeted to the new specification and +fails against the unchanged production behaviour. + +### SEAM-BLOCKED (19) — `ArchiveStemProjection.ToDisplayStem` seam + +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_PathStrictlyUnderRoot_ReturnsArchiveRelativeStem +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_PathEqualsRoot_ReturnsInputUnchanged +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_FalsePrefixSiblingArchive2_ReturnsInputUnchanged +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_RootWithOneTrailingSeparator_ReturnsArchiveRelativeStem +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_RootWithTwoTrailingSeparators_ReturnsArchiveRelativeStem +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_EmptyRoot_ReturnsInputUnchanged +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_WhitespaceOnlyRoot_ReturnsInputUnchanged +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_NullPath_ReturnsNull +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_EmptyPath_ReturnsInputUnchanged +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_ForwardSlashSeparators_ReturnsArchiveRelativeStem +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_MixedCaseRoot_ReturnsArchiveRelativeStem + +### SEAM-BLOCKED — `ArchiveChainProjection.TryTrimBelowArchiveRoot` seam + +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_ChainPassesThroughRoot_ReturnsSegmentsAfterTheRoot +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_ChainMissesTheRoot_ReturnsFalseAndEmptyOutput +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_LeafIsTheRoot_ReturnsFalseAndEmptyOutput +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_EmptyChain_ReturnsFalseAndEmptyOutput +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_SingleElementChainIsTheRoot_ReturnsFalse +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_RootWithTrailingSeparator_ReturnsSegmentsAfterTheRoot +- UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_FalsePrefixSiblingArchive2_ReturnsFalse + +### SEAM-BLOCKED — `OutlookFolderHierarchyProvider.IsAbsentLabel` seam + +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport + +### NEW (8) + +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence +- UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem +- UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderRowArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem +- UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderArray_OutOfRootRecentEntry_IsLeftUnchanged + +### RETARGETED (1) + +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTests.GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath + +## Acceptance check + +- `EXIT_CODE: 1` — met. +- The recorded failure set includes `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` — met. +- The recorded failure set includes `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem` — met. + +## New tests that are GREEN at the end of Phase 1 (3), recorded so the inventory is not misread + +These three new tests pin behaviour that already holds today, so a red result for them would have +been a finding rather than a result. They are NOT part of the [P1-T18] red inventory. + +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain +- UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderRowArray_AndFolderArray_AgreeOnRecentTextAfterProjection + +The remaining 12 passing tests are the untouched tests of +UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTests, which the [P1-T13] +retarget left unchanged. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. The TRX file +name is recorded with `` and `` substituted. No raw TRX content is pasted. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t17-qft-fail.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t17-qft-fail.md new file mode 100644 index 000000000..116fd1987 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t17-qft-fail.md @@ -0,0 +1,88 @@ +# [P1-T17] [expect-fail] QuickFiler.Test fail-before run + +Timestamp: 2026-09-07T07-17 + +Command: `vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation /Logger:trx /ResultsDirectory:TestResults\799-p1-t17 /Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None /TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterScoreJoinTests|FullyQualifiedName~QfcItemController_FolderHandlingTests` + +EXIT_CODE: 1 + +ExpectedExitCode: 1 + +## Output Summary + +`Test Run Failed.` — total 30, passed 25, failed 5. The failing set is the intended Phase 1 red set +for the QuickFiler surface: three new router tests that pin AC3, AC6 and AC7, and two retargeted +folder-handling tests that pin the AC4 removal of the empty-root strip. + +## TRX read + +TRX file (name reduced per R3): `__2026-09-07_07_16_50_net481.trx`, the most recently +modified TRX under the results directory for this task. Counter values are read from the +`ResultSummary/Counters` element. No raw TRX content is pasted (R3). + +- TOTAL: 30 +- PASSED: 25 +- FAILED: 5 + +## Suite selection (R13) + +This is a SINGLE-assembly run over QuickFiler.Test driven by an explicit `FullyQualifiedName~` +class filter over two classes. The four environmentally-hanging shell-icon classes live in +UtilitiesCS.Test and are not reachable by this filter or this assembly, so the R13 exclusion +clauses are unnecessary here and the reduced denominator is the 30 tests of the two named classes. +The filter contains no `&` clause, so the `&`-binds-tighter-than-`|` precedence hazard does not +apply. + +## Failing tests, by fully qualified name, with the reduced failure reason + +### NEW (3) + +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage + - Reason: the rendered document does not contain the `73%` cell. The score is keyed by the + archive-rooted path while the presented row is the archive-relative stem, so the join misses. + This is exactly the AC6 defect; [P2-T11] supplies the additive projected key. +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey + - Reason: same missing `73%` cell under a chain that begins below the archive root. The filing + target half of the assertion already holds; the score-key half does not. +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned + - Reason: `Did not expect document to contain "Stale"` — the zero-candidate label is still + rendered because no suppression exists yet. [P2-T12] supplies it. + +### RETARGETED (2) + +- QuickFiler.Controllers.Tests.QfcItemController_FolderHandlingTests.ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection + - Reason: the empty-archive-root case still strips one leading separator. The retargeted + expectation is the identity projection, per AC4. +- QuickFiler.Controllers.Tests.QfcItemController_FolderHandlingTests.AssignFolderComboBox_WhenEmptyArchiveRootAndLeadingSeparator_PreselectsProjectedFolder + - Reason: `Moq.MockException` — `SetFolderSelectedItem` expected once, observed 0 times. The + recorded invocations show the production path still calling `FolderContains` with the STRIPPED + value while the array entry carries the unstripped value, so the selection falls back to + `SetFolderSelectedIndex(1)`. This is the same AC4 removal observed at the `FolderContains` + boundary, and it turns green when [P2-T9] routes both sides through the shared projection. + +## Acceptance check + +- `EXIT_CODE: 1` — met. +- `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` appears in the failure set — met. +- `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` appears in the failure + set — met. + +## New tests that are GREEN at the end of Phase 1 (4), recorded so the inventory is not misread + +These four new router tests pin behaviour that already holds today — the no-regression and +restriction cases — so a red result for them would have been a finding rather than a result. They +are NOT part of the [P1-T18] red inventory. + +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_AmbiguousLabel_IsNotSuppressed + +The remaining 21 passing tests are the untouched tests of +QuickFiler.Controllers.Tests.QfcItemController_FolderHandlingTests. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. The TRX file +name is recorded with `` and `` substituted. Failure reasons are paraphrased from the +parsed TRX; no raw TRX content is pasted. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t18-red-inventory.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t18-red-inventory.md new file mode 100644 index 000000000..02203d4ed --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t18-red-inventory.md @@ -0,0 +1,83 @@ +# [P1-T18] End-of-Phase-1 red inventory + +Timestamp: 2026-09-07T07-17 + +Command: consolidation of the two fail-before artifacts written by [P1-T16] and [P1-T17]; no new +command was run. + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +## Output Summary + +33 tests are red at the end of Phase 1: 28 from the [P1-T16] UtilitiesCS.Test run and 5 from the +[P1-T17] QuickFiler.Test run. This is the set Phase 2 must turn green, and nothing else. + +## Arithmetic check + +- [P1-T16] recorded failure count: 28 +- [P1-T17] recorded failure count: 5 +- Sum: 33 +- Entries in this inventory: 33 + +## Tag totals + +- `SEAM-BLOCKED`: 19 +- `NEW`: 11 +- `RETARGETED`: 3 +- Total: 33 + +## Inventory + +Every entry carries exactly one tag. + +| # | Fully qualified name | Tag | +|---|---|---| +| 1 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_PathStrictlyUnderRoot_ReturnsArchiveRelativeStem | SEAM-BLOCKED | +| 2 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_PathEqualsRoot_ReturnsInputUnchanged | SEAM-BLOCKED | +| 3 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_FalsePrefixSiblingArchive2_ReturnsInputUnchanged | SEAM-BLOCKED | +| 4 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_RootWithOneTrailingSeparator_ReturnsArchiveRelativeStem | SEAM-BLOCKED | +| 5 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_RootWithTwoTrailingSeparators_ReturnsArchiveRelativeStem | SEAM-BLOCKED | +| 6 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_EmptyRoot_ReturnsInputUnchanged | SEAM-BLOCKED | +| 7 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_WhitespaceOnlyRoot_ReturnsInputUnchanged | SEAM-BLOCKED | +| 8 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_NullPath_ReturnsNull | SEAM-BLOCKED | +| 9 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_EmptyPath_ReturnsInputUnchanged | SEAM-BLOCKED | +| 10 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_ForwardSlashSeparators_ReturnsArchiveRelativeStem | SEAM-BLOCKED | +| 11 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_MixedCaseRoot_ReturnsArchiveRelativeStem | SEAM-BLOCKED | +| 12 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_ChainPassesThroughRoot_ReturnsSegmentsAfterTheRoot | SEAM-BLOCKED | +| 13 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_ChainMissesTheRoot_ReturnsFalseAndEmptyOutput | SEAM-BLOCKED | +| 14 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_LeafIsTheRoot_ReturnsFalseAndEmptyOutput | SEAM-BLOCKED | +| 15 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_EmptyChain_ReturnsFalseAndEmptyOutput | SEAM-BLOCKED | +| 16 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_SingleElementChainIsTheRoot_ReturnsFalse | SEAM-BLOCKED | +| 17 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_RootWithTrailingSeparator_ReturnsSegmentsAfterTheRoot | SEAM-BLOCKED | +| 18 | UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_FalsePrefixSiblingArchive2_ReturnsFalse | SEAM-BLOCKED | +| 19 | UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport | SEAM-BLOCKED | +| 20 | UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot | NEW | +| 21 | UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty | NEW | +| 22 | UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty | NEW | +| 23 | UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence | NEW | +| 24 | UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence | NEW | +| 25 | UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem | NEW | +| 26 | UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderRowArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem | NEW | +| 27 | UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderArray_OutOfRootRecentEntry_IsLeftUnchanged | NEW | +| 28 | UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTests.GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath | RETARGETED | +| 29 | QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage | NEW | +| 30 | QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey | NEW | +| 31 | QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned | NEW | +| 32 | QuickFiler.Controllers.Tests.QfcItemController_FolderHandlingTests.ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection | RETARGETED | +| 33 | QuickFiler.Controllers.Tests.QfcItemController_FolderHandlingTests.AssignFolderComboBox_WhenEmptyArchiveRootAndLeadingSeparator_PreselectsProjectedFolder | RETARGETED | + +## Notes for Phase 2 + +- Entry 33 was not named as a retarget target by decision D9, which named only entries 28 and 32. + It was carried into the retarget by [P1-T14]'s explicit instruction to re-derive the assertions of + the following test whose prose encoded the removed empty-root strip. Its assertions encoded that + strip, so they were re-derived. This is recorded here rather than left for a reviewer to discover. +- Seven new tests are GREEN at the end of Phase 1 and are deliberately absent from this inventory, + because they pin behaviour that already holds. They are enumerated in the [P1-T16] and [P1-T17] + artifacts. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t5-seam-build.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t5-seam-build.md new file mode 100644 index 000000000..7a3ff7964 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p1-t5-seam-build.md @@ -0,0 +1,56 @@ +# [P1-T5] Seam build — solution compiles with the Phase 1 declaration seams + +Timestamp: 2026-09-07T07-01 + +Command: `msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"` + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +## Derived counts + +- CS8625-COUNT: 0 +- CS8618-COUNT: 0 + +Both counts are the number of occurrences of that diagnostic code in the captured MSBuild +stdout, not an inference from the exit code. The three edited/created production files +(`ArchiveStemProjection.cs`, `ArchiveChainProjection.cs`, `OutlookFolderHierarchyProvider.cs`) +all carry `#nullable enable`, so nullable diagnostics are emitted as warnings by this build +even without `/p:TreatWarningsAsErrors=true`; the zero counts are therefore a real observation. + +## Output Summary + +MSBuild summary tail: + +``` + 0 Warning(s) + 0 Error(s) +``` + +Build succeeded with 0 warnings and 0 errors. This proves decision D2's claim that the new +optional second constructor parameter on `OutlookFolderHierarchyProvider` leaves all 19 existing +provider constructions compiling unchanged — 13 in +UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs and 6 in +UtilitiesCS.Test/OutlookObjects/Folder/FolderHierarchyProviderAdapterTests.cs — including the +single-null-argument construction at OutlookFolderHierarchyProviderTests.cs:316, which binds +unambiguously to the first parameter because the type declares exactly one constructor. + +## Command form (R10) + +`/t:Build` with no `/p:` gate switches. This build exists to produce test assemblies, not to run +gates; R10 reserves `/t:Rebuild` with the gate switches for [P3-T3] and [P3-T4]. Every source +edit in [P1-T1] through [P1-T4] changed a file timestamp, so `CoreCompile` was not skipped for +the affected projects. + +## Environment note + +MSBuild is not on this machine's PATH. The invoking shell prepended the Visual Studio 18 +Community MSBuild `Current\Bin\amd64` directory before invoking the command. The command text +itself is character-for-character the form this task specifies. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. The raw +MSBuild log was written outside the repository to a session scratch location and is not +committed. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md index 244dfcdd2..f9d9b1e6c 100644 --- a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md @@ -543,45 +543,45 @@ new tests name types and members that do not exist yet and a missing declaration time rather than producing a targeted failure. No production behaviour changes in Phase 1: every seam body either throws `NotImplementedException` or stores a value nothing reads yet. -- [ ] [P1-T1] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveStemProjection` and the single member `public static string? ToDisplayStem(string? folderPath, string? archiveRoot)` whose body is `throw new NotImplementedException("Issue #799: the display projection body is supplied by [P2-T1].");`. Both parameters and the return carry the `?` annotation because this file opens with `#nullable enable`, because [P1-T6] pins a null-path case, and because two of its three production callers — `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` and `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` — are themselves `#nullable enable` files whose argument expressions are nullable, so an unannotated parameter is CS8604 at those call sites under the [P3-T4] gate. The XML doc must state that this is a LENIENT DISPLAY projection returning the archive-relative stem when the path is strictly under the root and the input unchanged in every other case including an empty or whitespace root, and must state why it is a separate type rather than a member on the strict contract: that contract is a hard boundary that yields an empty string on failure and never passes its input through, and every display site needs the opposite fallback, so a lenient overload there would blur the invariant #614 created it for. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `ToDisplayStem`. +- [x] [P1-T1] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveStemProjection` and the single member `public static string? ToDisplayStem(string? folderPath, string? archiveRoot)` whose body is `throw new NotImplementedException("Issue #799: the display projection body is supplied by [P2-T1].");`. Both parameters and the return carry the `?` annotation because this file opens with `#nullable enable`, because [P1-T6] pins a null-path case, and because two of its three production callers — `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` and `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` — are themselves `#nullable enable` files whose argument expressions are nullable, so an unannotated parameter is CS8604 at those call sites under the [P3-T4] gate. The XML doc must state that this is a LENIENT DISPLAY projection returning the archive-relative stem when the path is strictly under the root and the input unchanged in every other case including an empty or whitespace root, and must state why it is a separate type rather than a member on the strict contract: that contract is a hard boundary that yields an empty string on failure and never passes its input through, and every display site needs the opposite fallback, so a lenient overload there would blur the invariant #614 created it for. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `ToDisplayStem`. -- [ ] [P1-T2] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveChainProjection` and the single member `public static bool TryTrimBelowArchiveRoot(IReadOnlyList? chain, string? archiveRoot, out IReadOnlyList trimmed)` whose body assigns an empty array to `trimmed` and then is `throw new NotImplementedException("Issue #799: the chain trim body is supplied by [P2-T2].");`. The `chain` and `archiveRoot` parameters carry the `?` annotation because [P2-T2] specifies a false return for a null chain and for a null or whitespace root, and this file opens with `#nullable enable`; `trimmed` stays unannotated because it is assigned an empty array on every path before return. The XML doc must state that the archive-root node is the first chain index whose segment FolderPath is the root itself, detected as TryMakeArchiveRelative returning true with an empty stem, that the method returns the remainder of the chain after that index, and that it returns false when no such index exists and also when that index is the last one, because the leaf is then the root and there is nothing to render below it. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `TryTrimBelowArchiveRoot`. +- [x] [P1-T2] Create `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` in namespace `UtilitiesCS.OutlookObjects.Folder`, with `#nullable enable`, declaring `public static class ArchiveChainProjection` and the single member `public static bool TryTrimBelowArchiveRoot(IReadOnlyList? chain, string? archiveRoot, out IReadOnlyList trimmed)` whose body assigns an empty array to `trimmed` and then is `throw new NotImplementedException("Issue #799: the chain trim body is supplied by [P2-T2].");`. The `chain` and `archiveRoot` parameters carry the `?` annotation because [P2-T2] specifies a false return for a null chain and for a null or whitespace root, and this file opens with `#nullable enable`; `trimmed` stays unannotated because it is assigned an empty array on every path before return. The XML doc must state that the archive-root node is the first chain index whose segment FolderPath is the root itself, detected as TryMakeArchiveRelative returning true with an empty stem, that the method returns the remainder of the chain after that index, and that it returns false when no such index exists and also when that index is the last one, because the leaf is then the root and there is nothing to render below it. Acceptance: the file compiles once [P1-T3] wires it, is at or below 500 lines, and a case-sensitive search of it finds the single-line token `TryTrimBelowArchiveRoot`. -- [ ] [P1-T3] Add two one-line self-closing Compile Include entries to `UtilitiesCS/UtilitiesCS.csproj` for the two new production helpers, adjacent to the existing ArchiveStemContract.cs entry at line 623. The project is legacy non-SDK with an insertion-ordered item list and no globbing, so the entries are mandatory and their placement follows the neighbouring folder entries. Acceptance: the project file contains exactly two new Compile Include lines and the two named files compile. +- [x] [P1-T3] Add two one-line self-closing Compile Include entries to `UtilitiesCS/UtilitiesCS.csproj` for the two new production helpers, adjacent to the existing ArchiveStemContract.cs entry at line 623. The project is legacy non-SDK with an insertion-ordered item list and no globbing, so the entries are mandatory and their placement follows the neighbouring folder entries. Acceptance: the project file contains exactly two new Compile Include lines and the two named files compile. -- [ ] [P1-T4] In `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`, add the declaration-only seams that Phase 1 tests bind against, changing no existing behaviour. Add `public interface IFolderLabelAbsenceReport` with the single member `bool IsAbsentLabel(string folderPath);` and an XML doc recording D4's reason for a separate interface rather than a fourth member on IFolderHierarchyProvider. Declare that the provider implements it, with `public bool IsAbsentLabel(string folderPath) => throw new NotImplementedException("Issue #799: the absence report body is supplied by [P2-T6].");`. Add the optional second constructor parameter `System.Func? archiveRootAccessor = null`, stored in the internal get-only auto-property `internal System.Func? ArchiveRootAccessor { get; }` (D3), leaving the existing null check on the first parameter exactly as it is. Add `internal System.Action? ErrorSink { get; set; }` with an XML doc naming it the injected diagnostic sink that tests observe instead of attaching a log4net appender, so no test mutates the process-global logger repository. Every one of these three declarations carries the `?` annotation because this file opens with `#nullable enable` at line 1 and the [P3-T4] gate runs `/p:TreatWarningsAsErrors=true`: an unannotated `System.Func … = null` is CS8625 and an unannotated never-initialised `System.Action` property is CS8618, and both are promoted to build errors by that gate. Do not change GetAncestorChainAsync, GetImmediateSubfoldersAsync, ResolveLeafKeyAsync or ResolveByUniqueSuffix in this task. Acceptance: the solution compiles; the constructor declares exactly two parameters, the second optional with a null default; and a case-sensitive search of the file finds the single-line tokens `IFolderLabelAbsenceReport`, `ArchiveRootAccessor` and `ErrorSink`; and no CS8625 or CS8618 diagnostic is introduced by these declarations, proved by the [P1-T5] artifact recording `CS8625-COUNT: 0` and `CS8618-COUNT: 0` and re-proved under enforcement at [P3-T4]. The two gate builds are NOT run in this task: R10 reserves `/t:Rebuild` with the gate switches for [P3-T3] and [P3-T4], and a Phase 1 invocation of either would be an unrecorded command named by no task and writing no artifact. +- [x] [P1-T4] In `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`, add the declaration-only seams that Phase 1 tests bind against, changing no existing behaviour. Add `public interface IFolderLabelAbsenceReport` with the single member `bool IsAbsentLabel(string folderPath);` and an XML doc recording D4's reason for a separate interface rather than a fourth member on IFolderHierarchyProvider. Declare that the provider implements it, with `public bool IsAbsentLabel(string folderPath) => throw new NotImplementedException("Issue #799: the absence report body is supplied by [P2-T6].");`. Add the optional second constructor parameter `System.Func? archiveRootAccessor = null`, stored in the internal get-only auto-property `internal System.Func? ArchiveRootAccessor { get; }` (D3), leaving the existing null check on the first parameter exactly as it is. Add `internal System.Action? ErrorSink { get; set; }` with an XML doc naming it the injected diagnostic sink that tests observe instead of attaching a log4net appender, so no test mutates the process-global logger repository. Every one of these three declarations carries the `?` annotation because this file opens with `#nullable enable` at line 1 and the [P3-T4] gate runs `/p:TreatWarningsAsErrors=true`: an unannotated `System.Func … = null` is CS8625 and an unannotated never-initialised `System.Action` property is CS8618, and both are promoted to build errors by that gate. Do not change GetAncestorChainAsync, GetImmediateSubfoldersAsync, ResolveLeafKeyAsync or ResolveByUniqueSuffix in this task. Acceptance: the solution compiles; the constructor declares exactly two parameters, the second optional with a null default; and a case-sensitive search of the file finds the single-line tokens `IFolderLabelAbsenceReport`, `ArchiveRootAccessor` and `ErrorSink`; and no CS8625 or CS8618 diagnostic is introduced by these declarations, proved by the [P1-T5] artifact recording `CS8625-COUNT: 0` and `CS8618-COUNT: 0` and re-proved under enforcement at [P3-T4]. The two gate builds are NOT run in this task: R10 reserves `/t:Rebuild` with the gate switches for [P3-T3] and [P3-T4], and a Phase 1 invocation of either would be an unrecorded command named by no task and writing no artifact. -- [ ] [P1-T5] Build the solution so the seam declarations are available to the test projects, and record `/evidence/regression-testing/p1-t5-seam-build.md`. Acceptance: `EXIT_CODE: 0`, which also proves D2's claim that the optional parameter leaves all 19 existing provider constructions compiling unchanged, including the single-null-argument construction at UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs:316; and the artifact records the two derived lines `CS8625-COUNT:` and `CS8618-COUNT:`, each the number of occurrences of that diagnostic code in the captured MSBuild output, both of which must be `0`. Nullable diagnostics are emitted as warnings by this build even without `/p:TreatWarningsAsErrors=true`, because the three edited files carry `#nullable enable`, so counting them here is a real observation rather than an inference from the exit code. +- [x] [P1-T5] Build the solution so the seam declarations are available to the test projects, and record `/evidence/regression-testing/p1-t5-seam-build.md`. Acceptance: `EXIT_CODE: 0`, which also proves D2's claim that the optional parameter leaves all 19 existing provider constructions compiling unchanged, including the single-null-argument construction at UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs:316; and the artifact records the two derived lines `CS8625-COUNT:` and `CS8618-COUNT:`, each the number of occurrences of that diagnostic code in the captured MSBuild output, both of which must be `0`. Nullable diagnostics are emitted as warnings by this build even without `/p:TreatWarningsAsErrors=true`, because the three edited files carry `#nullable enable`, so counting them here is a real observation rather than an inference from the exit code. ```powershell msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" ``` -- [ ] [P1-T6] Create `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` with `[TestClass] public sealed class ArchiveStemProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, using MSTest and FluentAssertions and no mocks, covering the #614 boundary cases the spec enumerates: a path strictly under the root; a path EQUAL to the root, which must return the input unchanged rather than an empty string; the Archive2 false-prefix case, where a path under a sibling folder named Archive2 tested against a root ending in Archive yields the character 2 at the root's length and is therefore NOT projected; a root supplied with one and with two trailing separators; an empty root and a whitespace-only root, both of which must yield the input unchanged, which is the AC4 one-separator strip elimination; a null path and an empty path; forward-slash separators on both parameters; and a mixed-case root. Acceptance: the file compiles once [P1-T10] wires it, contains at least nine `[TestMethod]` attributes, and is at or below 500 lines. +- [x] [P1-T6] Create `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` with `[TestClass] public sealed class ArchiveStemProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, using MSTest and FluentAssertions and no mocks, covering the #614 boundary cases the spec enumerates: a path strictly under the root; a path EQUAL to the root, which must return the input unchanged rather than an empty string; the Archive2 false-prefix case, where a path under a sibling folder named Archive2 tested against a root ending in Archive yields the character 2 at the root's length and is therefore NOT projected; a root supplied with one and with two trailing separators; an empty root and a whitespace-only root, both of which must yield the input unchanged, which is the AC4 one-separator strip elimination; a null path and an empty path; forward-slash separators on both parameters; and a mixed-case root. Acceptance: the file compiles once [P1-T10] wires it, contains at least nine `[TestMethod]` attributes, and is at or below 500 lines. -- [ ] [P1-T7] Create `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` with `[TestClass] public sealed class ArchiveChainProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, building every chain from `FolderBreadcrumbSegment` literals through the four-argument constructor at UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbSegment.cs:29-40, with no snapshot, no provider and no COM. It covers: a chain that passes through the root, asserting the returned segments are exactly the ones after the root node and that segment identity is preserved by reference; a chain that does not pass through the root, asserting false and an empty output; a chain whose LEAF is the root, asserting false; an empty chain; a single-element chain that is the root; a root supplied with a trailing separator; and the Archive2 false-prefix case at chain level. Acceptance: the file compiles once [P1-T10] wires it, contains at least seven `[TestMethod]` attributes, and is at or below 500 lines. +- [x] [P1-T7] Create `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` with `[TestClass] public sealed class ArchiveChainProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, building every chain from `FolderBreadcrumbSegment` literals through the four-argument constructor at UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbSegment.cs:29-40, with no snapshot, no provider and no COM. It covers: a chain that passes through the root, asserting the returned segments are exactly the ones after the root node and that segment identity is preserved by reference; a chain that does not pass through the root, asserting false and an empty output; a chain whose LEAF is the root, asserting false; an empty chain; a single-element chain that is the root; a root supplied with a trailing separator; and the Archive2 false-prefix case at chain level. Acceptance: the file compiles once [P1-T10] wires it, contains at least seven `[TestMethod]` attributes, and is at or below 500 lines. -- [ ] [P1-T8] Create `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` with `[TestClass] public sealed class OutlookFolderHierarchyProviderTrimTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, using a `Mock` returning a hand-built `FolderTreeSnapshot` in the pattern already used throughout UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs, plus a `Func` root accessor. It contains: `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` (a store-rooted three-level Archive chain; assert the returned segments begin at the first node BELOW the archive root and that neither the store node nor the archive-root node appears); `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain` (the no-accessor construction is the effective off switch); `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty` (asserting the AC2 error text is delivered exactly once through the injected `ErrorSink` and that the result is an empty segment list, which routes each surface into its existing fallback); `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty`; `GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain` (the accessor is lazy precisely because the archive-root property throws, per D2); `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence` (resolve an unresolvable label twice against the same provider instance, assert exactly one emission through `ErrorSink`, and assert `IsAbsentLabel` returns true for that path); `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` (a decoy snapshot in which two nodes share the suffix; assert one emission and that `IsAbsentLabel` returns FALSE, which is decision D-B's zero-candidate restriction); and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` (D6's reset). Acceptance: the file compiles once [P1-T10] wires it, contains exactly eight `[TestMethod]` attributes, and is at or below 500 lines. +- [x] [P1-T8] Create `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` with `[TestClass] public sealed class OutlookFolderHierarchyProviderTrimTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, using a `Mock` returning a hand-built `FolderTreeSnapshot` in the pattern already used throughout UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs, plus a `Func` root accessor. It contains: `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` (a store-rooted three-level Archive chain; assert the returned segments begin at the first node BELOW the archive root and that neither the store node nor the archive-root node appears); `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain` (the no-accessor construction is the effective off switch); `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty` (asserting the AC2 error text is delivered exactly once through the injected `ErrorSink` and that the result is an empty segment list, which routes each surface into its existing fallback); `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty`; `GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain` (the accessor is lazy precisely because the archive-root property throws, per D2); `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence` (resolve an unresolvable label twice against the same provider instance, assert exactly one emission through `ErrorSink`, and assert `IsAbsentLabel` returns true for that path); `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` (a decoy snapshot in which two nodes share the suffix; assert one emission and that `IsAbsentLabel` returns FALSE, which is decision D-B's zero-candidate restriction); and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` (D6's reset). Acceptance: the file compiles once [P1-T10] wires it, contains exactly eight `[TestMethod]` attributes, and is at or below 500 lines. -- [ ] [P1-T9] Create `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` with `[TestClass] public sealed class FolderPredictorRecentsProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, following the globals/recents mock construction already used at UtilitiesCS.Test/OutlookObjects/Folder/FolderRowTests.cs:246-258 and UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs:994. It contains `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`, `FolderRowArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`, `FolderRowArray_AndFolderArray_AgreeOnRecentTextAfterProjection` (the text-parity contract documented at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`:233-242 and currently unasserted), and `FolderArray_OutOfRootRecentEntry_IsLeftUnchanged`. Each test seeds the recents list with one archive-rooted entry and one already-relative entry, so the projection is observable and the identity case is pinned in the same fixture. Acceptance: the file compiles once [P1-T10] wires it, contains exactly four `[TestMethod]` attributes, and is at or below 500 lines. +- [x] [P1-T9] Create `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` with `[TestClass] public sealed class FolderPredictorRecentsProjectionTests` in namespace `UtilitiesCS.Test.OutlookObjects.Folder`, following the globals/recents mock construction already used at UtilitiesCS.Test/OutlookObjects/Folder/FolderRowTests.cs:246-258 and UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs:994. It contains `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`, `FolderRowArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`, `FolderRowArray_AndFolderArray_AgreeOnRecentTextAfterProjection` (the text-parity contract documented at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`:233-242 and currently unasserted), and `FolderArray_OutOfRootRecentEntry_IsLeftUnchanged`. Each test seeds the recents list with one archive-rooted entry and one already-relative entry, so the projection is observable and the identity case is pinned in the same fixture. Acceptance: the file compiles once [P1-T10] wires it, contains exactly four `[TestMethod]` attributes, and is at or below 500 lines. -- [ ] [P1-T10] Add four one-line self-closing Compile Include entries to `UtilitiesCS.Test/UtilitiesCS.Test.csproj` for the four new test files, appended adjacent to the existing folder-test entries at lines 302-304. The project is legacy non-SDK with an insertion-ordered item list and no globbing. Acceptance: the project file contains exactly four new Compile Include lines and the four named files compile. +- [x] [P1-T10] Add four one-line self-closing Compile Include entries to `UtilitiesCS.Test/UtilitiesCS.Test.csproj` for the four new test files, appended adjacent to the existing folder-test entries at lines 302-304. The project is legacy non-SDK with an insertion-ordered item list and no globbing. Acceptance: the project file contains exactly four new Compile Include lines and the four named files compile. -- [ ] [P1-T11] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` with `[TestClass] public sealed class BreadcrumbBridgeRouterScoreJoinTests` in namespace `QuickFiler.Test.Controllers`, modelled on the strict-mock construction already used at QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs:254-259 (a `Mock`, a `Mock` capturing NavigateToString output, a real `BreadcrumbMessageCodec`, a real `BreadcrumbHtmlRenderer` and a real `BreadcrumbOutboundQueue`). It contains: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` (the AC6 pin: presented row is the archive-relative stem, the score carries the raw rooted path, the bound root is non-empty; assert the rendered document contains the percentage cell for that row); `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage` (D7's additive requirement: the rooted-presented case must not regress); `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` (the public three-argument overload forwards an empty root, so the projection is the identity and no existing caller changes behaviour); `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` (the AC3 pin, asserted as its own test and not as an incidental consequence: with an ancestor chain that begins below the archive root, the bound row's filing target and the joined score key are both still the archive-relative stem); and `BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly` (the spec's integration scenario, driven entirely through the router with no WebView2 and no Outlook: a banner row, a suggestion row, a search-result row, the trash pseudo-row and one stale label; assert lineage on both folder row kinds, the existing fallback on the stale label, and no lineage on the banner or trash rows); `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned` (the AC7 row half, and the only test in this plan that takes the TRUE arm of the [P2-T12] suppression predicate: the provider mock is extended with `.As()` so the router's `provider as IFolderLabelAbsenceReport` cast succeeds, `IsAbsentLabel` is set up to return true for exactly one suggestion row's hierarchy path and false for the others, and `FetchChainAsync` is driven to a null chain for that row; assert the rendered document contains no row for the suppressed label, that every surviving row still carries its own segment keys, and that the surviving row count is one lower than the presented row count); and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed` (decision D-B's zero-candidate restriction at the router boundary: same construction, but `IsAbsentLabel` returns false for the null-chain row, and the row must still be rendered with the existing fallback). Extending the mock with `.As()` is confined to this file and reaches no existing test: no other router test in QuickFiler.Test uses `.As<>()`, so in every one of them the cast still yields null and suppression stays inert exactly as D4 records. Call `.As()` on the mock BEFORE that mock's `.Object` is first materialized, because Moq throws at run time when an interface is added to a mock whose object has already been created, and stating the ordering here is what keeps that failure from surfacing for the first time as a red test at [P1-T17]. Author this file for C# 7.3. QuickFiler.Test.csproj declares no `` and targets v4.8.1 at its line 18, so it compiles at the 7.3 default while every other project in this plan's scope is at Latest, preview or 12.0. Use classic `using (...) { }` blocks, `!= null` rather than `is not null`, explicitly typed `new` rather than target-typed `new`, `switch` statements rather than switch expressions, and no nullable reference annotation anywhere in the file. Mirroring a construct from an existing UtilitiesCS.Test file into this one is the specific failure mode: it surfaces as CS8370 at the [P1-T15] build, not at edit time. Acceptance: the file compiles once [P1-T12] wires it, contains exactly seven `[TestMethod]` attributes, and is at or below 500 lines. +- [x] [P1-T11] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` with `[TestClass] public sealed class BreadcrumbBridgeRouterScoreJoinTests` in namespace `QuickFiler.Test.Controllers`, modelled on the strict-mock construction already used at QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue614Tests.cs:254-259 (a `Mock`, a `Mock` capturing NavigateToString output, a real `BreadcrumbMessageCodec`, a real `BreadcrumbHtmlRenderer` and a real `BreadcrumbOutboundQueue`). It contains: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` (the AC6 pin: presented row is the archive-relative stem, the score carries the raw rooted path, the bound root is non-empty; assert the rendered document contains the percentage cell for that row); `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage` (D7's additive requirement: the rooted-presented case must not regress); `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` (the public three-argument overload forwards an empty root, so the projection is the identity and no existing caller changes behaviour); `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` (the AC3 pin, asserted as its own test and not as an incidental consequence: with an ancestor chain that begins below the archive root, the bound row's filing target and the joined score key are both still the archive-relative stem); and `BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly` (the spec's integration scenario, driven entirely through the router with no WebView2 and no Outlook: a banner row, a suggestion row, a search-result row, the trash pseudo-row and one stale label; assert lineage on both folder row kinds, the existing fallback on the stale label, and no lineage on the banner or trash rows); `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned` (the AC7 row half, and the only test in this plan that takes the TRUE arm of the [P2-T12] suppression predicate: the provider mock is extended with `.As()` so the router's `provider as IFolderLabelAbsenceReport` cast succeeds, `IsAbsentLabel` is set up to return true for exactly one suggestion row's hierarchy path and false for the others, and `FetchChainAsync` is driven to a null chain for that row; assert the rendered document contains no row for the suppressed label, that every surviving row still carries its own segment keys, and that the surviving row count is one lower than the presented row count); and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed` (decision D-B's zero-candidate restriction at the router boundary: same construction, but `IsAbsentLabel` returns false for the null-chain row, and the row must still be rendered with the existing fallback). Extending the mock with `.As()` is confined to this file and reaches no existing test: no other router test in QuickFiler.Test uses `.As<>()`, so in every one of them the cast still yields null and suppression stays inert exactly as D4 records. Call `.As()` on the mock BEFORE that mock's `.Object` is first materialized, because Moq throws at run time when an interface is added to a mock whose object has already been created, and stating the ordering here is what keeps that failure from surfacing for the first time as a red test at [P1-T17]. Author this file for C# 7.3. QuickFiler.Test.csproj declares no `` and targets v4.8.1 at its line 18, so it compiles at the 7.3 default while every other project in this plan's scope is at Latest, preview or 12.0. Use classic `using (...) { }` blocks, `!= null` rather than `is not null`, explicitly typed `new` rather than target-typed `new`, `switch` statements rather than switch expressions, and no nullable reference annotation anywhere in the file. Mirroring a construct from an existing UtilitiesCS.Test file into this one is the specific failure mode: it surfaces as CS8370 at the [P1-T15] build, not at edit time. Acceptance: the file compiles once [P1-T12] wires it, contains exactly seven `[TestMethod]` attributes, and is at or below 500 lines. -- [ ] [P1-T12] Add one one-line self-closing Compile Include entry to `QuickFiler.Test/QuickFiler.Test.csproj` for the new test file, appended adjacent to the existing BreadcrumbBridgeRouterIssue439Tests.cs entry at line 64. Acceptance: the project file contains exactly one new Compile Include line and the named file compiles. +- [x] [P1-T12] Add one one-line self-closing Compile Include entry to `QuickFiler.Test/QuickFiler.Test.csproj` for the new test file, appended adjacent to the existing BreadcrumbBridgeRouterIssue439Tests.cs entry at line 64. Acceptance: the project file contains exactly one new Compile Include line and the named file compiles. -- [ ] [P1-T13] Retarget GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments in `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` lines 72-91 so the pinned configuration is the one production uses (D9): rename it to `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`, construct the provider with a root accessor returning the fixture's `\Root` path, and replace the root-to-leaf assertion at lines 84-87 with the trimmed expectation `\Root\Clients`, `\Root\Clients\Acme`. Retargeting rather than deleting is required because the deleted behaviour is a specification change and a deleted test pins nothing. No companion case is added HERE. The off switch is pinned instead by `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain` in the new file `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` that [P1-T8] creates, which has ample headroom. This placement is a hard constraint rather than a preference: the file edited here is 479 lines against a 500-line ceiling, its existing test body is 20 lines, and a same-shape companion plus its blank separator would consume 21 of the 21 available lines before the one line the added constructor argument itself costs and before CSharpier reflows anything, which would put the file over the ceiling and make the [P3-T10] gate unsatisfiable with no remedy. Acceptance: a case-sensitive search of the file finds zero matches for the single-line token `GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments` and exactly one match for `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`; the twelve other provider constructions in the file are unchanged; and the file is at or below 483 lines, that is its [P0-T14] baseline of 479 plus at most four lines. +- [x] [P1-T13] Retarget GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments in `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` lines 72-91 so the pinned configuration is the one production uses (D9): rename it to `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`, construct the provider with a root accessor returning the fixture's `\Root` path, and replace the root-to-leaf assertion at lines 84-87 with the trimmed expectation `\Root\Clients`, `\Root\Clients\Acme`. Retargeting rather than deleting is required because the deleted behaviour is a specification change and a deleted test pins nothing. No companion case is added HERE. The off switch is pinned instead by `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain` in the new file `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` that [P1-T8] creates, which has ample headroom. This placement is a hard constraint rather than a preference: the file edited here is 479 lines against a 500-line ceiling, its existing test body is 20 lines, and a same-shape companion plus its blank separator would consume 21 of the 21 available lines before the one line the added constructor argument itself costs and before CSharpier reflows anything, which would put the file over the ceiling and make the [P3-T10] gate unsatisfiable with no remedy. Acceptance: a case-sensitive search of the file finds zero matches for the single-line token `GetAncestorChainAsync_HappyPath_ReturnsRootToLeafSegments` and exactly one match for `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`; the twelve other provider constructions in the file are unchanged; and the file is at or below 483 lines, that is its [P0-T14] baseline of 479 plus at most four lines. -- [ ] [P1-T14] Retarget the empty-root assertion in `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` lines 219-226, inside ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection, so it asserts the identity projection rather than the one-separator strip AC4 eliminates: an empty archive root must now leave the input unchanged, and the assertion's `because` text must state that AC4 of issue #799 removed the empty-root strip. Update the surrounding XML doc at lines 206-211 so it no longer describes the removed behaviour, and update the XML doc at lines 245-258 of the following test, whose prose asserts that an empty archive root causes FolderArray entries to be stripped; that following test's own assertions are re-derived against the new behaviour and updated only where they encode the removed strip. The five other boundary assertions in the retargeted test (null root, null path, out-of-root path, empty-remainder guard, case-insensitive prefix) are unchanged, because ToDisplayStem reproduces each of them. Acceptance: a case-sensitive search of the file finds zero matches for the single-line literal `archivePrefix of one separator, which it strips`; the retargeted test still contains exactly six assertions on ProjectPredeterminedFolder; and the file is at or below 500 lines. +- [x] [P1-T14] Retarget the empty-root assertion in `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` lines 219-226, inside ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection, so it asserts the identity projection rather than the one-separator strip AC4 eliminates: an empty archive root must now leave the input unchanged, and the assertion's `because` text must state that AC4 of issue #799 removed the empty-root strip. Update the surrounding XML doc at lines 206-211 so it no longer describes the removed behaviour, and update the XML doc at lines 245-258 of the following test, whose prose asserts that an empty archive root causes FolderArray entries to be stripped; that following test's own assertions are re-derived against the new behaviour and updated only where they encode the removed strip. The five other boundary assertions in the retargeted test (null root, null path, out-of-root path, empty-remainder guard, case-insensitive prefix) are unchanged, because ToDisplayStem reproduces each of them. Acceptance: a case-sensitive search of the file finds zero matches for the single-line literal `archivePrefix of one separator, which it strips`; the retargeted test still contains exactly six assertions on ProjectPredeterminedFolder; and the file is at or below 500 lines. -- [ ] [P1-T15] Build the solution with the new and retargeted tests in place and record `/evidence/regression-testing/p1-t15-test-build.md`. Acceptance: `EXIT_CODE: 0`, proving every new test compiles against the Phase 1 seams. +- [x] [P1-T15] Build the solution with the new and retargeted tests in place and record `/evidence/regression-testing/p1-t15-test-build.md`. Acceptance: `EXIT_CODE: 0`, proving every new test compiles against the Phase 1 seams. ```powershell msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" ``` -- [ ] [P1-T16] [expect-fail] Run the four new and one retargeted UtilitiesCS.Test classes and record `/evidence/regression-testing/p1-t16-ut-fail.md` with `ExpectedExitCode: 1`. The artifact must enumerate, by fully qualified name, every failing test and state for each whether it fails because a Phase 1 seam throws `NotImplementedException` (tag `SEAM-BLOCKED`) or because the production behaviour is not yet changed (tag `NEW` or `RETARGETED`). Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: `EXIT_CODE: 1`, and the recorded failure set includes `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` and `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`. +- [x] [P1-T16] [expect-fail] Run the four new and one retargeted UtilitiesCS.Test classes and record `/evidence/regression-testing/p1-t16-ut-fail.md` with `ExpectedExitCode: 1`. The artifact must enumerate, by fully qualified name, every failing test and state for each whether it fails because a Phase 1 seam throws `NotImplementedException` (tag `SEAM-BLOCKED`) or because the production behaviour is not yet changed (tag `NEW` or `RETARGETED`). Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: `EXIT_CODE: 1`, and the recorded failure set includes `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot` and `FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem`. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -589,7 +589,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf & $vstest UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p1-t16' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~ArchiveStemProjectionTests|FullyQualifiedName~ArchiveChainProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTrimTests|FullyQualifiedName~FolderPredictorRecentsProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTests' ``` -- [ ] [P1-T17] [expect-fail] Run the new QuickFiler.Test score-join class and the retargeted folder-handling class and record `/evidence/regression-testing/p1-t17-qft-fail.md` with `ExpectedExitCode: 1`, enumerating each failing test by fully qualified name with its failure message reduced per R3. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: `EXIT_CODE: 1`, and both `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` and `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` appear in the failure set. +- [x] [P1-T17] [expect-fail] Run the new QuickFiler.Test score-join class and the retargeted folder-handling class and record `/evidence/regression-testing/p1-t17-qft-fail.md` with `ExpectedExitCode: 1`, enumerating each failing test by fully qualified name with its failure message reduced per R3. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: `EXIT_CODE: 1`, and both `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage` and `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` appear in the failure set. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -597,7 +597,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p1-t17' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterScoreJoinTests|FullyQualifiedName~QfcItemController_FolderHandlingTests' ``` -- [ ] [P1-T18] Write `/evidence/regression-testing/p1-t18-red-inventory.md` consolidating the two fail-before artifacts into one list of every test that is red at the end of Phase 1, each tagged `NEW`, `RETARGETED` or `SEAM-BLOCKED`. This is the set Phase 2 must turn green and nothing else. Acceptance: the inventory's entry count equals the sum of the failure counts recorded by [P1-T16] and [P1-T17], and every entry carries exactly one of the three tags. +- [x] [P1-T18] Write `/evidence/regression-testing/p1-t18-red-inventory.md` consolidating the two fail-before artifacts into one list of every test that is red at the end of Phase 1, each tagged `NEW`, `RETARGETED` or `SEAM-BLOCKED`. This is the set Phase 2 must turn green and nothing else. Acceptance: the inventory's entry count equals the sum of the failure counts recorded by [P1-T16] and [P1-T17], and every entry carries exactly one of the three tags. --- From f50fb7271406c9d52bce9b2ab89657f04dfb625c Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Mon, 7 Sep 2026 07:43:17 -0400 Subject: [PATCH 10/13] fix(799): implement archive-root lineage trim and suggestion path projection Phase 2 of the atomic plan: production implementation for issue #799. - ArchiveStemProjection.ToDisplayStem: lenient display projection over ArchiveStemContract, returning the input unchanged for a null/empty/ whitespace root, which removes the empty-root one-separator strip (AC4). - ArchiveChainProjection.TryTrimBelowArchiveRoot: yields the lineage below the archive-root node, false when the chain misses the root or the leaf is the root (AC1, AC2). - OutlookFolderHierarchyProvider: applies the trim inside GetAncestorChainAsync behind a lazy, fault-tolerant root accessor; adds the AC7 once-per-label log gate and the zero-candidate absence classification as two distinct per-instance concurrent sets, with the absence signal cleared on both resolution success routes. - FolderPredictor: ProjectSuggestionPath delegates to the shared projection, both recents surfaces are projected (AC5), and the include-children branch of GetOlSubpath becomes a verified prefix removal. - QfcItemController.ProjectPredeterminedFolder delegates to the same shared projection, so the two members agree by construction (AC4). - BreadcrumbBridgeRouter: adds an archive-relative alias alongside every raw score key so archive-rooted suggestions keep their percentage (AC6), and filters known-absent suggestion rows before row construction so row ids and segment keys stay aligned (AC7, Efc surface only per decision D5). - Relocates EnsureBreadcrumbPipeline into a new QfcItemController partial before adding the root-accessor argument, so ViewerSetup.cs never passes through 501 lines. All 33 tests left deliberately red by Phase 1 now pass, the seven deliberately-green pins remain green, and both suites are fully passing (UtilitiesCS.Test 4816/4816, QuickFiler.Test 1370/1370) with no newly failing test. Co-Authored-By: Claude Fable 5.1 --- .../Controllers/BreadcrumbBridgeRouter.cs | 109 ++++++++++++- QuickFiler/Controllers/EfcFormController.cs | 3 +- .../QfcItemController.BreadcrumbWiring.cs | 41 +++++ .../QfcItemController.FolderHandling.cs | 49 ++---- .../QfcItemController.ViewerSetup.cs | 33 ---- QuickFiler/QuickFiler.csproj | 1 + .../Folder/ArchiveChainProjection.cs | 44 +++++- .../Folder/ArchiveStemProjection.cs | 23 ++- .../OutlookObjects/Folder/FolderPredictor.cs | 41 +++-- .../Folder/OutlookFolderHierarchyProvider.cs | 122 +++++++++++++-- .../evidence/qa-gates/p2-t18-sizes-interim.md | 84 ++++++++++ .../regression-testing/p2-t15-build.md | 38 +++++ .../regression-testing/p2-t16-pass-after.md | 143 ++++++++++++++++++ .../regression-testing/p2-t17-suites.md | 74 +++++++++ .../plan.2026-09-06T22-01.md | 36 ++--- 15 files changed, 714 insertions(+), 127 deletions(-) create mode 100644 QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p2-t18-sizes-interim.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t15-build.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t16-pass-after.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t17-suites.md diff --git a/QuickFiler/Controllers/BreadcrumbBridgeRouter.cs b/QuickFiler/Controllers/BreadcrumbBridgeRouter.cs index 0232cdf90..e8d1dedd9 100644 --- a/QuickFiler/Controllers/BreadcrumbBridgeRouter.cs +++ b/QuickFiler/Controllers/BreadcrumbBridgeRouter.cs @@ -23,6 +23,12 @@ public sealed partial class BreadcrumbBridgeRouter ); private readonly IFolderHierarchyProvider _provider; + + // #799 AC7: obtained by an `as` cast in the constructor, so no constructor signature + // changes and no existing test breaks. A Mock<IFolderHierarchyProvider> is not an + // IFolderLabelAbsenceReport, so this stays null and suppression is inert in every existing + // router test. + private readonly IFolderLabelAbsenceReport? _absenceReport; private readonly IBreadcrumbWebHost _host; private readonly BreadcrumbMessageCodec _codec; private readonly BreadcrumbHtmlRenderer _renderer; @@ -47,6 +53,7 @@ BreadcrumbOutboundQueue outboundQueue ) { _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _absenceReport = provider as IFolderLabelAbsenceReport; _host = host ?? throw new ArgumentNullException(nameof(host)); _codec = codec ?? throw new ArgumentNullException(nameof(codec)); _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); @@ -104,6 +111,7 @@ CancellationToken cancellationToken var chains = new Dictionary>( StringComparer.OrdinalIgnoreCase ); + HashSet? suppressed = null; _boundRoot = string.IsNullOrWhiteSpace(archiveRootPath) ? string.Empty : archiveRootPath.TrimEnd('\\', '/'); @@ -126,15 +134,34 @@ CancellationToken cancellationToken if (chain != null) { chains[text] = chain; + continue; + } + + // #799 AC7 (Efc surface only, per decision D5). A null chain arising from + // cancellation or from a provider fault is NOT suppressed: those rows are not + // known-absent, and only the zero-candidate classification is. + if ( + hierarchyPath != null + && _absenceReport != null + && _absenceReport.IsAbsentLabel(hierarchyPath) + ) + { + suppressed ??= new HashSet(StringComparer.OrdinalIgnoreCase); + suppressed.Add(text); } } + IReadOnlyList retainedRows = RetainedRows(presentedRows, suppressed); _rows = _builder.BuildRows( - presentedRows, + retainedRows, text => chains.TryGetValue(text, out var chain) ? chain : null, - scores + WithProjectedScoreKeys(scores) ); - AttachSegmentKeys(presentedRows, chains); + + // The SAME retained list is handed to both calls: AttachSegmentKeys indexes the + // presented rows by row index, so an unfiltered list here would mis-align every row + // after the suppressed one. + AttachSegmentKeys(retainedRows, chains); _selectedRowId = null; // #499: the rows just rebuilt are a new set, so a folder path selected against the @@ -149,6 +176,82 @@ CancellationToken cancellationToken DeliverDocument(); } + /// + /// AC6: emits every original score UNCHANGED and, additionally, one archive-relative alias + /// for each score whose path is archive-rooted. The addition is what makes it safe — a + /// substitution would fix the stem-presented case and silently break the rooted-presented + /// case — and the row builder's probability index assigns through its indexer, so a + /// duplicate key is tolerated rather than throwing. + /// + private IEnumerable WithProjectedScoreKeys(IEnumerable scores) + { + // An empty bound root makes the projection the identity, so the public three-argument + // overload's callers see no change and allocate nothing. A null sequence is passed + // through, null-forgiving, so the row builder keeps raising its own + // ArgumentNullException rather than this method raising a different one. + if (scores == null || _boundRoot.Length == 0) + { + return scores!; + } + + var joined = new List(); + foreach (FolderScore score in scores) + { + joined.Add(score); + if (score.FolderPath == null) + { + continue; + } + + // Null-forgiving: ToDisplayStem returns null only for a null folderPath, which the + // guard above excludes; unsuppressed the construction below is CS8604. + string projected = ArchiveStemProjection.ToDisplayStem( + score.FolderPath, + _boundRoot + )!; + if (!string.Equals(projected, score.FolderPath, StringComparison.Ordinal)) + { + joined.Add(new FolderScore(projected, score.Score, score.Probability)); + } + } + + return joined; + } + + /// + /// AC7: the presented sequence with the known-absent labels removed, filtered BEFORE row + /// construction because row ids are assigned as row-<index> over this sequence. + /// Returns the original instance when nothing was suppressed. + /// + private IReadOnlyList RetainedRows( + IReadOnlyList presentedRows, + HashSet? suppressed + ) + { + if (suppressed == null || suppressed.Count == 0) + { + return presentedRows; + } + + var retained = new List(presentedRows.Count); + foreach (string text in presentedRows) + { + if (!string.IsNullOrEmpty(text) && suppressed.Contains(text)) + { + continue; + } + + // Null-forgiving: a null entry is carried through exactly as the unfiltered list + // carried it, so the row builder's handling of it is unchanged. + retained.Add(text!); + } + + log.Debug( + $"#799 AC7: suppressed {suppressed.Count} zero-candidate breadcrumb row(s) of {presentedRows.Count} presented." + ); + return retained; + } + private string? ToHierarchyPath(string presentedTarget) { // #609 preserved: a RELATIVE presented target stays root-prefixed for the lookup. diff --git a/QuickFiler/Controllers/EfcFormController.cs b/QuickFiler/Controllers/EfcFormController.cs index d8610ed44..9019b939d 100644 --- a/QuickFiler/Controllers/EfcFormController.cs +++ b/QuickFiler/Controllers/EfcFormController.cs @@ -1051,7 +1051,8 @@ private void ConfigureBreadcrumbControl() new WebView2CoreInitializer() ); var provider = new UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider( - _globals.Ol.FolderTreeService + _globals.Ol.FolderTreeService, + () => _globals.Ol.ArchiveRootPath ); _router = new BreadcrumbBridgeRouter( provider, diff --git a/QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs b/QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs new file mode 100644 index 000000000..cccffb320 --- /dev/null +++ b/QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs @@ -0,0 +1,41 @@ +using QuickFiler.Viewers; + +namespace QuickFiler.Controllers +{ + internal partial class QfcItemController + { + // #351: idempotently creates the host-neutral breadcrumb pipeline on the concrete viewer + // so folder population/selection are correct even before WebView2 core init completes. + // The 9101 provider is DI-resolved from the injected globals' folder-tree service seam — + // no live Outlook query is issued inside breadcrumb code (G6). Skipped for mock viewers + // (unit tests drive the coordinator directly through its own seams). + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal void EnsureBreadcrumbPipeline() + { + if (!(_itemViewer is ItemViewer viewer)) + { + return; + } + + if (viewer.BreadcrumbCoordinator == null) + { + var provider = new UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider( + _globals.Ol.FolderTreeService, + () => _globals.Ol.ArchiveRootPath + ); + viewer.InitializeBreadcrumbPipeline(provider); + } + + if (!ReferenceEquals(_breadcrumbViewer, viewer)) + { + if (_breadcrumbViewer != null) + { + _breadcrumbViewer.BreadcrumbUnhandledArrow -= OnBreadcrumbUnhandledArrow; + } + _breadcrumbViewer = viewer; + _breadcrumbViewer.BreadcrumbUnhandledArrow -= OnBreadcrumbUnhandledArrow; + _breadcrumbViewer.BreadcrumbUnhandledArrow += OnBreadcrumbUnhandledArrow; + } + } + } +} diff --git a/QuickFiler/Controllers/QfcItemController.FolderHandling.cs b/QuickFiler/Controllers/QfcItemController.FolderHandling.cs index ffb3b1b2c..d11e691de 100644 --- a/QuickFiler/Controllers/QfcItemController.FolderHandling.cs +++ b/QuickFiler/Controllers/QfcItemController.FolderHandling.cs @@ -224,10 +224,10 @@ public void AssignFolderComboBox() // FolderPredictor.ProjectSuggestionPath, while the carried PredeterminedFolder is // the RAW suggestion path the scorer read from Suggestions. Without projecting the // carried value the same way, FolderContains misses every archive-rooted - // suggestion and the selection silently falls back to the index-1 entry. The - // projection is duplicated here rather than reused because - // FolderPredictor.ProjectSuggestionPath is private and lives under UtilitiesCS, - // which this change may not modify. + // suggestion and the selection silently falls back to the index-1 entry. #799 AC4: + // both sides now route through the one shared projection + // ArchiveStemProjection.ToDisplayStem, so they agree by construction rather than by + // duplication. string predetermined = ProjectPredeterminedFolder( _predeterminedFolder, _globals is null ? null : (_globals.Ol?.ArchiveRootPath ?? string.Empty) @@ -250,38 +250,21 @@ public void AssignFolderComboBox() } /// - /// #678 AC12. Projects a raw suggestion path onto the form FolderPredictor.FolderArray - /// stores, so a containment probe against the combo box can match: strip - /// plus a trailing separator from the front of - /// , case-insensitively, but only when the remainder is - /// non-empty. #678 R2: the projection mirrors FolderPredictor.ProjectSuggestionPath - /// for every non-null and non-null - /// . A NULL stands for - /// that member's _globals is null guard and yields the identity; an EMPTY one does - /// not, because that member forms its prefix unconditionally and so strips a single leading - /// separator in that state. - /// - /// Two divergences from that member remain and are deliberate, and both are null-safety - /// differences rather than projection differences. First, a null or empty - /// is returned unchanged rather than dereferenced; - /// ProjectSuggestionPath does not guard it because its input comes from - /// Suggestions. Second, a non-null globals with a null Ol is treated by the - /// call site as an empty archive root rather than reproducing that member's null - /// dereference. + /// #678 AC12, re-derived under #799 AC4. Projects a raw suggestion path onto the form + /// FolderPredictor.FolderArray stores, so a containment probe against the combo box + /// can match. This member and FolderPredictor.ProjectSuggestionPath now share ONE + /// projection, ArchiveStemProjection.ToDisplayStem, so they agree by construction + /// rather than by duplication, and the empty-root one-separator strip that used to make + /// them diverge was eliminated by AC4: a null, empty, or whitespace-only + /// is now the identity projection, as is a null or empty + /// and any path that is not strictly under the root. /// internal static string ProjectPredeterminedFolder(string folderPath, string archiveRootPath) { - if (string.IsNullOrEmpty(folderPath) || archiveRootPath is null) - { - return folderPath; - } - - string archivePrefix = archiveRootPath + "\\"; - return - folderPath.StartsWith(archivePrefix, StringComparison.OrdinalIgnoreCase) - && folderPath.Length > archivePrefix.Length - ? folderPath.Substring(archivePrefix.Length) - : folderPath; + return UtilitiesCS.OutlookObjects.Folder.ArchiveStemProjection.ToDisplayStem( + folderPath, + archiveRootPath + ); } /// diff --git a/QuickFiler/Controllers/QfcItemController.ViewerSetup.cs b/QuickFiler/Controllers/QfcItemController.ViewerSetup.cs index 7fefde65f..f10797758 100644 --- a/QuickFiler/Controllers/QfcItemController.ViewerSetup.cs +++ b/QuickFiler/Controllers/QfcItemController.ViewerSetup.cs @@ -129,39 +129,6 @@ await ConfigureAndAttachBreadcrumbAsync( //}, Token, TaskContinuationOptions.OnlyOnRanToCompletion, ui); } - // #351: idempotently creates the host-neutral breadcrumb pipeline on the concrete viewer - // so folder population/selection are correct even before WebView2 core init completes. - // The 9101 provider is DI-resolved from the injected globals' folder-tree service seam — - // no live Outlook query is issued inside breadcrumb code (G6). Skipped for mock viewers - // (unit tests drive the coordinator directly through its own seams). - [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - internal void EnsureBreadcrumbPipeline() - { - if (!(_itemViewer is ItemViewer viewer)) - { - return; - } - - if (viewer.BreadcrumbCoordinator == null) - { - var provider = new UtilitiesCS.OutlookObjects.Folder.OutlookFolderHierarchyProvider( - _globals.Ol.FolderTreeService - ); - viewer.InitializeBreadcrumbPipeline(provider); - } - - if (!ReferenceEquals(_breadcrumbViewer, viewer)) - { - if (_breadcrumbViewer != null) - { - _breadcrumbViewer.BreadcrumbUnhandledArrow -= OnBreadcrumbUnhandledArrow; - } - _breadcrumbViewer = viewer; - _breadcrumbViewer.BreadcrumbUnhandledArrow -= OnBreadcrumbUnhandledArrow; - _breadcrumbViewer.BreadcrumbUnhandledArrow += OnBreadcrumbUnhandledArrow; - } - } - /// Configures the lazy popup with the existing environment and active theme. internal void ConfigureBreadcrumbDropDown( ItemViewer viewer, diff --git a/QuickFiler/QuickFiler.csproj b/QuickFiler/QuickFiler.csproj index 47a5c27cc..35def2f76 100644 --- a/QuickFiler/QuickFiler.csproj +++ b/QuickFiler/QuickFiler.csproj @@ -333,6 +333,7 @@ + diff --git a/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs b/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs index 0421b2691..a73318eb7 100644 --- a/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs +++ b/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs @@ -46,9 +46,47 @@ out IReadOnlyList trimmed ) { trimmed = Array.Empty(); - throw new NotImplementedException( - "Issue #799: the chain trim body is supplied by [P2-T2]." - ); + + // The null half of this guard is required, not defensive: ArchiveStemContract declares + // both inputs as non-nullable string, so passing archiveRoot through without narrowing + // is CS8604 under the nullable gate. A whitespace-only root is rejected by the + // contract's own guard, so it needs no separate test here. + if (chain is null || chain.Count == 0 || archiveRoot is null) + { + return false; + } + + for (int index = 0; index < chain.Count; index++) + { + bool isRootNode = + ArchiveStemContract.TryMakeArchiveRelative( + chain[index].FolderPath, + archiveRoot, + out var stem + ) && stem.Length == 0; + + if (!isRootNode) + { + continue; + } + + // The leaf IS the archive root: there is nothing below it to render. + if (index == chain.Count - 1) + { + return false; + } + + var below = new FolderBreadcrumbSegment[chain.Count - index - 1]; + for (int offset = 0; offset < below.Length; offset++) + { + below[offset] = chain[index + 1 + offset]; + } + + trimmed = below; + return true; + } + + return false; } } } diff --git a/UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs b/UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs index 3a7f064c3..f28579faf 100644 --- a/UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs +++ b/UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs @@ -1,5 +1,4 @@ #nullable enable -using System; namespace UtilitiesCS.OutlookObjects.Folder { @@ -40,9 +39,25 @@ public static class ArchiveStemProjection /// public static string? ToDisplayStem(string? folderPath, string? archiveRoot) { - throw new NotImplementedException( - "Issue #799: the display projection body is supplied by [P2-T1]." - ); + // The null guard is required, not defensive: ArchiveStemContract.TryMakeArchiveRelative + // declares both inputs as non-nullable string, so passing either parameter through + // without narrowing is CS8604 under the nullable gate. + if (folderPath is null || archiveRoot is null) + { + return folderPath; + } + + // A zero-length stem is the path-equals-root case, which the contract reports as true. + // An empty display row is worse than the full path, so it is not projected. + if ( + ArchiveStemContract.TryMakeArchiveRelative(folderPath, archiveRoot, out var stem) + && stem.Length > 0 + ) + { + return stem; + } + + return folderPath; } } } diff --git a/UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs b/UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs index c94b93972..7aa449d07 100644 --- a/UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs +++ b/UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs @@ -9,6 +9,7 @@ using System.Windows.Forms; using Microsoft.Office.Interop.Outlook; using UtilitiesCS; +using UtilitiesCS.OutlookObjects.Folder; using Outlook = Microsoft.Office.Interop.Outlook; namespace UtilitiesCS @@ -790,7 +791,11 @@ public void AddRecents(ref List folderList) // internal if (_globals.AF.RecentsList.Count > 0) { folderList.Add("======= RECENT SELECTIONS ========"); - folderList.AddRange(_globals.AF.RecentsList); + // AC5: recents share the suggestion projection; the ! is required (else CS8620). + var r = _globals.Ol.ArchiveRootPath; + folderList.AddRange( + _globals.AF.RecentsList.Select(x => ArchiveStemProjection.ToDisplayStem(x, r)!) + ); } } @@ -847,17 +852,9 @@ private void AddSuggestionRows(List rows) private string ProjectSuggestionPath(string folderPath) { - if (_globals is null) - { - return folderPath; - } - - var archivePrefix = _globals.Ol.ArchiveRootPath + "\\"; - return - folderPath.StartsWith(archivePrefix, StringComparison.OrdinalIgnoreCase) - && folderPath.Length > archivePrefix.Length - ? folderPath.Substring(archivePrefix.Length) - : folderPath; + // Null-forgiving: ToDisplayStem returns null only for a null input, which this + // non-nullable parameter excludes; unsuppressed the return is CS8603 (#799 AC4). + return ArchiveStemProjection.ToDisplayStem(folderPath, _globals?.Ol.ArchiveRootPath)!; } // Row-model mirror of AddRecents: the RECENT SELECTIONS separator (Separator, no score) @@ -874,9 +871,13 @@ private void AddRecentRows(List rows) null ) ); + // AC5 row-model mirror: projecting one surface only would break the documented + // text-parity contract. Null-forgiving as in AddRecents (else CS8604 at FolderRow). + var root = _globals.Ol.ArchiveRootPath; foreach (var recent in _globals.AF.RecentsList) { - rows.Add(new FolderRow(recent, FolderRowKind.Recent, null)); + var text = ArchiveStemProjection.ToDisplayStem(recent, root)!; + rows.Add(new FolderRow(text, FolderRowKind.Recent, null)); } } } @@ -954,14 +955,12 @@ public string GetOlSubpath(string path, string olAncestor, bool includeChildren) { if (includeChildren) { - if (olAncestor.EndsWith('\\'.ToString())) - { - return path.Substring(olAncestor.Length); - } - else - { - return path.Substring(olAncestor.Length + 1); - } + // #799 verified prefix removal: a non-prefix path now yields the input instead of a + // garbage substring, and a path no longer than the ancestor no longer throws. The + // contract is root-agnostic despite its parameter name. + return ArchiveStemContract.TryMakeArchiveRelative(path, olAncestor, out var stem) + ? stem + : path; } else { diff --git a/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs b/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs index b43430b32..240c418c1 100644 --- a/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs +++ b/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -47,6 +48,22 @@ public sealed class OutlookFolderHierarchyProvider private readonly IOutlookFolderTreeService _treeService; + // Two DISTINCT per-instance structures (#799 D6), not one. The reported set only ever gains + // entries, which is what makes the AC7 diagnostic once per label per session rather than + // once per render. The absent set also LOSES entries, because a label that becomes + // resolvable after a snapshot refresh must stop being suppressed. ConcurrentDictionary + // rather than HashSet because ResolveLeafKeyAsync awaits AcquireSnapshotAsync and its + // continuations are not guaranteed to resume on one thread; per-instance rather than static + // because a static set is process-wide mutable state shared across viewers and across test + // methods in a single assembly. + private readonly ConcurrentDictionary _reportedLabels = new( + StringComparer.OrdinalIgnoreCase + ); + + private readonly ConcurrentDictionary _absentLabels = new( + StringComparer.OrdinalIgnoreCase + ); + /// /// Creates a provider over the supplied folder-tree service. /// @@ -91,7 +108,64 @@ CancellationToken cancellationToken { var snapshot = await AcquireSnapshotAsync(cancellationToken).ConfigureAwait(false); var chain = FolderTreeSnapshotQueries.GetAncestorChain(snapshot, leafKey); - return MapNodes(chain); + var mapped = MapNodes(chain); + + // The trim runs AFTER the snapshot walk and BEFORE the caller sees the chain, so row + // order, banner placement and the trash pseudo-row are all untouched (#799 AC1, AC2). + string? archiveRoot = TryReadArchiveRoot(); + if (string.IsNullOrWhiteSpace(archiveRoot)) + { + return mapped; + } + + if (ArchiveChainProjection.TryTrimBelowArchiveRoot(mapped, archiveRoot, out var trimmed)) + { + return trimmed; + } + + // AC2: a chain that never reaches the archive root is a diagnosable condition. Returning + // an empty list routes the Efc surface into the empty-chain single-segment fallback and + // the QuickFiler surface into its existing scored fallback. + EmitError( + $"Resolved ancestor chain does not pass through the configured archive root '{archiveRoot}'; falling back to single-segment rendering." + ); + return Array.Empty(); + } + + /// + /// Reads the configured archive root through the injected accessor, treating a null + /// accessor and any exception from it alike as "no trim configured". The accessor is lazy + /// and its faults are swallowed here because the underlying archive-root property throws + /// when the root is unresolvable and two of the three construction sites are outside any + /// try block, so a propagating read would create a new throw site at those call sites. + /// + private string? TryReadArchiveRoot() + { + var accessor = ArchiveRootAccessor; + if (accessor is null) + { + return null; + } + + try + { + return accessor(); + } + catch (Exception exception) + { + logger.Debug( + "The archive-root accessor threw; leaving the ancestor chain untrimmed.", + exception + ); + return null; + } + } + + /// Emits one diagnostic through log4net and through the injected test sink. + private void EmitError(string message) + { + logger.Error(message); + ErrorSink?.Invoke(message); } /// @@ -126,17 +200,24 @@ CancellationToken cancellationToken if (match != null) { + // The exact-path route returns before the suffix pass is ever reached, so the AC7 + // absence signal has to be cleared here as well as on the suffix success route. + _absentLabels.TryRemove(folderPath, out _); return match.Key; } - return ResolveByUniqueSuffix(snapshot, folderPath); + var resolved = ResolveByUniqueSuffix(snapshot, folderPath); + if (resolved != null) + { + _absentLabels.TryRemove(folderPath, out _); + } + + return resolved; } /// public bool IsAbsentLabel(string folderPath) => - throw new NotImplementedException( - "Issue #799: the absence report body is supplied by [P2-T6]." - ); + !string.IsNullOrWhiteSpace(folderPath) && _absentLabels.ContainsKey(folderPath); /// /// Second resolution pass for a relative stem such as Projects\Alpha, which the @@ -145,8 +226,12 @@ public bool IsAbsentLabel(string folderPath) => /// exactly one node qualifies: uniqueness is the safety property that prevents filing into /// a same-named folder under a different root. Zero or multiple candidates return null, so /// the caller keeps today's single-segment fallback rendering. + /// + /// An instance member rather than a static one because the AC7 log gate and the absence + /// classification are both per-provider-instance state (#799 D6). + /// /// - private static FolderTreeNodeKey? ResolveByUniqueSuffix( + private FolderTreeNodeKey? ResolveByUniqueSuffix( FolderTreeSnapshot snapshot, string folderPath ) @@ -164,11 +249,26 @@ string folderPath return candidates[0].Key; } - logger.Error( - candidates.Length == 0 - ? $"No snapshot node path ends with '{suffix}'; leaving '{folderPath}' unresolved." - : $"Multiple snapshot node paths end with '{suffix}'; leaving '{folderPath}' unresolved." - ); + if (candidates.Length == 0) + { + // AC7, restricted by decision D-B to the ZERO-candidate cause: the label is absent + // from the snapshot. Ambiguity is not absence — the folder does exist — so the + // multiple-candidate cause deliberately leaves this signal untouched. + _absentLabels[folderPath] = 0; + } + + // TryAdd is the AC7 log gate: a label already reported by this provider instance emits + // nothing further, so the diagnostic is once per label per session rather than once per + // render. The two causes stay distinguishable in the message text. + if (_reportedLabels.TryAdd(folderPath, 0)) + { + EmitError( + candidates.Length == 0 + ? $"No snapshot node path ends with '{suffix}'; leaving '{folderPath}' unresolved." + : $"Multiple snapshot node paths end with '{suffix}'; leaving '{folderPath}' unresolved." + ); + } + return null; } diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p2-t18-sizes-interim.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p2-t18-sizes-interim.md new file mode 100644 index 000000000..1d1abea6b --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p2-t18-sizes-interim.md @@ -0,0 +1,84 @@ +# [P2-T18] Interim (pre-format) line counts of every file this plan has edited or created + +Timestamp: 2026-09-07T07-40 + +Command: `(Get-Content -LiteralPath ).Count` for each path below + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +CEILING: 500 (applies to *.cs only) + +These counts are taken BEFORE the [P3-T1] CSharpier pass. The formatter can change line counts, so +[P3-T10] re-measures the same set afterwards and is the gating measurement. + +## Production `.cs` + +| Path | [P0-T14] baseline | Now | D11 budget | Verdict | +|---|---|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` | NOT PRESENT | 63 | 500 | met | +| `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` | NOT PRESENT | 92 | 500 | met | +| `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` | 141 | 300 | 500 | met | +| `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` | 1003 | 1002 | 1003 | met | +| `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` | 312 | 295 | 500, and [P2-T10] additionally requires at or below its 312 baseline | met | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 304 | 407 | 500 | met | +| `QuickFiler/Controllers/EfcFormController.cs` | 1320 | 1321 | 1322 | met | +| `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` | NOT PRESENT | 41 | 500 | met | +| `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` | 500 | 467 | 500 (hard) | met | + +## Test `.cs` + +| Path | [P0-T14] baseline | Now | Budget | Verdict | +|---|---|---|---|---| +| `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` | NOT PRESENT | 176 | 500 | met | +| `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` | NOT PRESENT | 217 | 500 | met | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` | NOT PRESENT | 344 | 500 | met | +| `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` | NOT PRESENT | 213 | 500 | met | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` | NOT PRESENT | 425 | 500 | met | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` | 479 | 480 | 483 ([P1-T13] +4) | met | +| `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` | 354 | 363 | 500 | met | + +## PROJECT-FILE (exempt) + +Recorded as exempt observations rather than asserted against the ceiling, per R8: the 500-line cap +in .claude/rules/general-code-change.md covers production code, test code and reusable script files +and does not reach project files, and .csharpierignore lines 9-14 record that project files are +owned by Visual Studio and are not C# source. + +- PROJECT-FILE (exempt): `UtilitiesCS/UtilitiesCS.csproj` = 1317 (baseline 1315, +2 Compile Include) +- PROJECT-FILE (exempt): `QuickFiler/QuickFiler.csproj` = 606 (baseline 605, +1 Compile Include) +- PROJECT-FILE (exempt): `UtilitiesCS.Test/UtilitiesCS.Test.csproj` = 980 (baseline 976, +4 Compile Include) +- PROJECT-FILE (exempt): `QuickFiler.Test/QuickFiler.Test.csproj` = 530 (baseline 529, +1 Compile Include) + +## Files within ten lines of their budget (named explicitly, with remaining headroom) + +- `QuickFiler/Controllers/EfcFormController.cs` — 1321 against a budget of 1322. Remaining headroom: + 1 line. The single added lazy root-accessor argument cost exactly one line, which is inside the + at-most-two-line allowance D11 derives for it. +- `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` — 1002 against a no-growth budget of 1003. + Remaining headroom: 1 line. The net figure is the sum of the D11-derived collapses (the + `ProjectSuggestionPath` body and the include-children branch of `GetOlSubpath`) against the two + recents projections and one added using directive. + +No other `.cs` file in this set is within ten lines of its budget. The next closest are +`UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` at 480 against 483 +(3 lines of headroom, so it IS within ten and is named here for completeness) and +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` at 467 against 500 (33 lines). + +Corrected enumeration of the within-ten set, so the list above is not read as exhaustive: +`EfcFormController.cs` (1), `FolderPredictor.cs` (1) and +`UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` (3). + +## Output Summary + +Every listed `.cs` count satisfies its D11 budget. The four project-file counts are recorded under +the exempt heading and are not asserted against the ceiling. Three `.cs` files are within ten lines +of their budget and are named above with their remaining headroom. The R9 ordering constraint held: +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` never passed through 501 lines, because +[P2-T3] removed 33 lines from it before [P2-T14] added the constructor argument to the relocated +member in the new partial. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t15-build.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t15-build.md new file mode 100644 index 000000000..be6eded84 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t15-build.md @@ -0,0 +1,38 @@ +# [P2-T15] Post-implementation solution build + +Timestamp: 2026-09-07T07-36 + +Command: `msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"` + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +## Output Summary + +`Build succeeded.` with `0 Warning(s)` and `0 Error(s)`. Every Phase 2 production edit compiles: +the two projection helper bodies, the provider trim and AC7 gate, the FolderPredictor delegations, +the relocated item-controller partial and its new Compile Include entry, the router score join and +row suppression, and the two lazy root-accessor construction arguments. + +Per plan rule R10 this is an iterative Phase 2 build: `/t:Build` with no `/p:` gate switches. The two +gate builds using `/t:Rebuild` belong to [P3-T3] and [P3-T4]. + +## Nullable observation (recorded because it changed the diff) + +The FIRST invocation of this command exited 0 but printed `2 Warning(s)`, both in +`QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`: + +- `CS8603: Possible null reference return.` at the pass-through return of `WithProjectedScoreKeys`. +- `CS8604: Possible null reference argument for parameter 'item' in 'void List.Add(string item)'` + in `RetainedRows`. + +`/p:TreatWarningsAsErrors=true` promotes both to build errors, so the [P3-T4] gate would have failed +on them. Both were repaired in place with a null-forgiving operator and a one-line reason comment, +matching the pattern the plan already prescribes for the other `ToDisplayStem` call sites, and this +recorded run is the re-run after that repair. No suppression pragma was added and no diagnostic was +disabled. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t16-pass-after.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t16-pass-after.md new file mode 100644 index 000000000..ee48c10da --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t16-pass-after.md @@ -0,0 +1,143 @@ +# [P2-T16] Pass-after run of every test in the [P1-T18] red inventory + +Timestamp: 2026-09-07T07-36 + +Command: + +``` + UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation /Logger:trx /ResultsDirectory:TestResults\799-p2-t16-ut /Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None /TestCaseFilter:FullyQualifiedName~ArchiveStemProjectionTests|FullyQualifiedName~ArchiveChainProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTrimTests|FullyQualifiedName~FolderPredictorRecentsProjectionTests|FullyQualifiedName~OutlookFolderHierarchyProviderTests + QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation /Logger:trx /ResultsDirectory:TestResults\799-p2-t16-qft /Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None /TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterScoreJoinTests|FullyQualifiedName~QfcItemController_FolderHandlingTests|FullyQualifiedName~BreadcrumbBridgeRouterIssue439Tests +``` + +`` is the vswhere-resolved vstest.console.exe path pinned by [P0-T7]. + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +EXIT-CODE-UT: 0 + +EXIT-CODE-QFT: 0 + +FAILED-UT: 0 + +FAILED-QFT: 0 + +P2-T16-TOTAL-RUN: 83 + +P2-T16-TOTAL-PASSED: 83 + +## Output Summary + +Both invocations printed `Test Run Successful.` The UtilitiesCS.Test run reported +`Total tests: 43 / Passed: 43`; the QuickFiler.Test run reported `Total tests: 40 / Passed: 40`. +`FAILED-UT` and `FAILED-QFT` are read from each run's TRX `ResultSummary/Counters` `failed` +attribute, not from the console, because vstest prints no `Failed:` line at all on a fully passing +run. + +All 33 tests of the [P1-T18] red inventory are `Passed`. The two run totals are larger than 33 +because the filters select whole classes and therefore also re-run tests that were already green at +the end of Phase 1; per the task text those totals are recorded but not asserted against the +inventory count. + +## TRX documents read (R3-reduced names) + +- UtilitiesCS.Test: `TestResults\799-p2-t16-ut\__2026-09-07_07_36_26_net481.trx` +- QuickFiler.Test: `TestResults\799-p2-t16-qft\__2026-09-07_07_36_38_net481.trx` + +Each results directory held exactly one TRX at read time. + +## PASS-AFTER lines (33, one per [P1-T18] inventory entry) + +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_PathStrictlyUnderRoot_ReturnsArchiveRelativeStem +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_PathEqualsRoot_ReturnsInputUnchanged +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_FalsePrefixSiblingArchive2_ReturnsInputUnchanged +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_RootWithOneTrailingSeparator_ReturnsArchiveRelativeStem +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_RootWithTwoTrailingSeparators_ReturnsArchiveRelativeStem +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_EmptyRoot_ReturnsInputUnchanged +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_WhitespaceOnlyRoot_ReturnsInputUnchanged +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_NullPath_ReturnsNull +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_EmptyPath_ReturnsInputUnchanged +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_ForwardSlashSeparators_ReturnsArchiveRelativeStem +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveStemProjectionTests.ToDisplayStem_MixedCaseRoot_ReturnsArchiveRelativeStem +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_ChainPassesThroughRoot_ReturnsSegmentsAfterTheRoot +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_ChainMissesTheRoot_ReturnsFalseAndEmptyOutput +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_LeafIsTheRoot_ReturnsFalseAndEmptyOutput +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_EmptyChain_ReturnsFalseAndEmptyOutput +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_SingleElementChainIsTheRoot_ReturnsFalse +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_RootWithTrailingSeparator_ReturnsSegmentsAfterTheRoot +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.ArchiveChainProjectionTests.TryTrimBelowArchiveRoot_FalsePrefixSiblingArchive2_ReturnsFalse +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderRowArray_RootedRecentEntry_IsProjectedToTheArchiveRelativeStem +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderArray_OutOfRootRecentEntry_IsLeftUnchanged +PASS-AFTER: UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTests.GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath +PASS-AFTER: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage +PASS-AFTER: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey +PASS-AFTER: QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned +PASS-AFTER: QuickFiler.Controllers.Tests.QfcItemController_FolderHandlingTests.ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection +PASS-AFTER: QuickFiler.Controllers.Tests.QfcItemController_FolderHandlingTests.AssignFolderComboBox_WhenEmptyArchiveRootAndLeadingSeparator_PreselectsProjectedFolder + +PASS-AFTER line count: 33. [P1-T18] inventory count: 33. Equal. + +## The seven deliberately-green pins are still green + +These are not inventory entries; they are recorded here because a red result for any of them would +be a regression introduced by Phase 2 rather than an expected transition. + +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain — Passed +- UtilitiesCS.Test.OutlookObjects.Folder.OutlookFolderHierarchyProviderTrimTests.GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain — Passed +- UtilitiesCS.Test.OutlookObjects.Folder.FolderPredictorRecentsProjectionTests.FolderRowArray_AndFolderArray_AgreeOnRecentTextAfterProjection — Passed +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage — Passed +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged — Passed +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly — Passed +- QuickFiler.Test.Controllers.BreadcrumbBridgeRouterScoreJoinTests.BindRowsAsync_AmbiguousLabel_IsNotSuppressed — Passed + +## The ten D8 no-hunk #439 tests are still green + +The QuickFiler.Test filter deliberately includes `BreadcrumbBridgeRouterIssue439Tests`, because +[P2-T12] states their continued passing as the observable proof that suppression is inert behind a +provider mock that does not implement the absence report. All ten are `Passed`: +Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability, +Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively, +Issue439UnresolvedChainsUseSelectableFallbackForEveryDiagnosableProviderOutcome, +Issue439InvalidTypedNavigationDoesNotSelectBannerOrPseudoRows, +Issue439ArchiveRootBoundarySelectionAndHostEventRemainDeterministic, +Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection, +Issue609_DirectRowSelection_UsesFullLookupAndRelativeFilingTarget, +Issue609_AncestorActivation_EmitsArchiveRelativeFilingTarget, +Issue609_ImmediateChildActivation_EmitsArchiveRelativeFilingTarget and +Issue439AncestorActivationQueriesAncestorKeyAndSelectsArchiveRelativeChild. + +## Supplementary run: the two ToDoModel.Test assertions named by [P2-T9] + +[P2-T9]'s acceptance condition names two ToDoModel.Test assertions in directory Email Utilities, +file FolderHandlerTests_Written.cs, and the plan records that no Phase 2 command covers that +assembly. They were therefore run separately so the condition rests on evidence rather than on +deferral to [P3-T5]: + +``` + ToDoModel.Test\bin\Debug\ToDoModel.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation /Logger:trx /ResultsDirectory:TestResults\799-p2-t9-todomodel /Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None /TestCaseFilter:FullyQualifiedName~GetRelevantOlPathPortion +``` + +`Test Run Successful.` with `Total tests: 2 / Passed: 2`, exit code 0: + +- PASS-AFTER: GetRelevantOlPathPortion_StateUnderTest_ExpectedBehavior1 +- PASS-AFTER: GetRelevantOlPathPortion_StateUnderTest_ExpectedBehavior2 + +TRX read (R3-reduced name): +`TestResults\799-p2-t9-todomodel\__2026-09-07_07_41_15_net481.trx`. + +These two are not [P1-T18] inventory entries and are not counted in the 33 above; they are the +must-stay-green side of the [P2-T9] rewrite. [P3-T5] remains the scheduled full-set check. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. The TRX file +names are recorded with the account and machine segments replaced by `` and ``, and no +TRX content was pasted: only parsed counter values and test names were read. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t17-suites.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t17-suites.md new file mode 100644 index 000000000..6c664196f --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/regression-testing/p2-t17-suites.md @@ -0,0 +1,74 @@ +# [P2-T17] Whole-assembly post-change suite run + +Timestamp: 2026-09-07T07-39 + +Command: + +``` + UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation /Logger:trx /ResultsDirectory:TestResults\799-p2-t17-ut /Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None /TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests + QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation /Logger:trx /ResultsDirectory:TestResults\799-p2-t17-qft /Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None /TestCaseFilter:TestCategory!=LiveOutlook +``` + +`` is the vswhere-resolved vstest.console.exe path pinned by [P0-T7]. + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +EXIT-CODE-UT: 0 + +EXIT-CODE-QFT: 0 + +POST-UT-TOTAL: 4816 + +POST-UT-PASSED: 4816 + +POST-UT-FAILED: 0 + +POST-QFT-TOTAL: 1370 + +POST-QFT-PASSED: 1370 + +POST-QFT-FAILED: 0 + +NEWLY-FAILING: NONE + +## Output Summary + +Both invocations printed `Test Run Successful.` Every `POST-*-FAILED` value is read from its run's +TRX `ResultSummary/Counters` `failed` attribute and not from the console, because vstest prints no +`Failed:` line at all on a fully passing run. + +Comparison against the [P0-T11] baseline: + +| Counter | Baseline | Post-change | Verdict | +|---|---|---|---| +| UT total | 4786 | 4816 | +30, the four new UtilitiesCS.Test classes (11 + 7 + 8 + 4) | +| UT passed | 4786 | 4816 | +30 | +| UT failed | 0 | 0 | `POST-UT-FAILED` <= `BASELINE-UT-FAILED` — met | +| QFT total | 1363 | 1370 | +7, the new QuickFiler.Test score-join class | +| QFT passed | 1363 | 1370 | +7 | +| QFT failed | 0 | 0 | `POST-QFT-FAILED` <= `BASELINE-QFT-FAILED` — met | + +`NEWLY-FAILING: NONE` is derivable directly from the counters: zero tests failed in either run, so +the set of tests failing here that were not failing in the baseline is empty. + +## Suite selection (R13) + +The UtilitiesCS.Test run carries the same four `FullyQualifiedName!~` shell-icon exclusions the +[P0-T11] baseline carried — `HelperClasses.ShellUtilities_Tests`, +`HelperClasses.ShellUtilitiesStatic_Tests`, `HelperClasses.SysImageListHelperTests` and +`EmailIntelligence.OSBrowser_Tests` — so the reduced denominator is identical on both sides of the +comparison above. Those four classes call SHGetFileInfo and stall vstest on this machine. + +## TRX documents read (R3-reduced names) + +- UtilitiesCS.Test: `TestResults\799-p2-t17-ut\__2026-09-07_07_38_28_net481.trx` +- QuickFiler.Test: `TestResults\799-p2-t17-qft\__2026-09-07_07_39_09_net481.trx` + +Each results directory held exactly one TRX at read time. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. No TRX content +was pasted: only parsed counter values were read. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md index f9d9b1e6c..2d8f59a38 100644 --- a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/plan.2026-09-06T22-01.md @@ -603,41 +603,41 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf ### Phase 2 — Production implementation -- [ ] [P2-T1] Replace the [P1-T1] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` with the real implementation: guard first with `if (folderPath is null || archiveRoot is null) { return folderPath; }`, then return the stem when `ArchiveStemContract.TryMakeArchiveRelative(folderPath, archiveRoot, out var stem)` returns true AND `stem.Length > 0`, and return `folderPath` unchanged in every other case. The leading null guard is mandatory rather than defensive: `ArchiveStemContract.TryMakeArchiveRelative` declares its two input parameters as non-nullable `string` at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:107-108 inside a file that opens with `#nullable enable` at line 1, so passing either [P1-T1] `string?` parameter into it without first narrowing is CS8604, which the [P3-T4] gate promotes to a build error. The guard also delivers the behaviour [P1-T6] pins for the null-path case, so it costs no extra branch. The length condition is what makes "path equals root" non-projectable, because the contract returns true with an empty stem on exact equality at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:124-127 and an empty display row is worse than the full path; it reproduces the existing guard at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`:858 exactly. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the display projection body is supplied by`. +- [x] [P2-T1] Replace the [P1-T1] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` with the real implementation: guard first with `if (folderPath is null || archiveRoot is null) { return folderPath; }`, then return the stem when `ArchiveStemContract.TryMakeArchiveRelative(folderPath, archiveRoot, out var stem)` returns true AND `stem.Length > 0`, and return `folderPath` unchanged in every other case. The leading null guard is mandatory rather than defensive: `ArchiveStemContract.TryMakeArchiveRelative` declares its two input parameters as non-nullable `string` at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:107-108 inside a file that opens with `#nullable enable` at line 1, so passing either [P1-T1] `string?` parameter into it without first narrowing is CS8604, which the [P3-T4] gate promotes to a build error. The guard also delivers the behaviour [P1-T6] pins for the null-path case, so it costs no extra branch. The length condition is what makes "path equals root" non-projectable, because the contract returns true with an empty stem on exact equality at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:124-127 and an empty display row is worse than the full path; it reproduces the existing guard at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`:858 exactly. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the display projection body is supplied by`. -- [ ] [P2-T2] Replace the [P1-T2] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` with the real implementation: scan the chain for the first index whose segment FolderPath satisfies `ArchiveStemContract.TryMakeArchiveRelative(segment.FolderPath, archiveRoot, out var stem)` returning true with `stem.Length == 0`, which is exactly the equality case; assign the remainder after that index to `trimmed` and return true; return false with an empty `trimmed` when no such index exists, when the chain is null or empty, and when that index is the last element. A null `chain` or a null `archiveRoot` returns false through an explicit leading guard, and a whitespace-only `archiveRoot` returns false through the contract's own guard at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:113. The null half of that guard is mandatory rather than defensive: the contract declares its two input parameters as non-nullable `string` at ArchiveStemContract.cs:107-108 inside a file that opens with `#nullable enable` at line 1, so passing the [P1-T2] `string? archiveRoot` into it without first narrowing is CS8604, which the [P3-T4] gate promotes to a build error. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the chain trim body is supplied by`. +- [x] [P2-T2] Replace the [P1-T2] seam body in `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` with the real implementation: scan the chain for the first index whose segment FolderPath satisfies `ArchiveStemContract.TryMakeArchiveRelative(segment.FolderPath, archiveRoot, out var stem)` returning true with `stem.Length == 0`, which is exactly the equality case; assign the remainder after that index to `trimmed` and return true; return false with an empty `trimmed` when no such index exists, when the chain is null or empty, and when that index is the last element. A null `chain` or a null `archiveRoot` returns false through an explicit leading guard, and a whitespace-only `archiveRoot` returns false through the contract's own guard at UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs:113. The null half of that guard is mandatory rather than defensive: the contract declares its two input parameters as non-nullable `string` at ArchiveStemContract.cs:107-108 inside a file that opens with `#nullable enable` at line 1, so passing the [P1-T2] `string? archiveRoot` into it without first narrowing is CS8604, which the [P3-T4] gate promotes to a build error. Acceptance: every `[TestMethod]` in `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` passes, and a case-sensitive search of the file returns zero matches for the single-line literal `Issue #799: the chain trim body is supplied by`. -- [ ] [P2-T3] Relocate the breadcrumb pipeline helper OUT of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` and INTO the new file `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task runs BEFORE [P2-T14] adds the constructor argument; the reverse order puts the viewer-setup file at 501 lines in an intermediate state (R9). Move lines 132-163 verbatim — the two-part comment at 132-136, the `[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]` attribute at 137, and the member body at 138-163 — together with the blank line that separated it from its neighbour. The new file declares `internal partial class QfcItemController` in namespace `QuickFiler.Controllers` and carries only the using directives the moved code needs, which are the ones for the concrete viewer type and for the arrow-event handler; the provider is referenced by its fully qualified name in the moved code and needs no using. This is a pure relocation: no statement is added, removed or reordered. The invocation at that file's line 112 is NOT moved and NOT removed; it is an ordinary call from a sibling method of the same partial class and resolves across the partial once [P2-T4] adds the new file to the build. Acceptance: a case-sensitive search of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` returns zero matches for the single-line token `internal void EnsureBreadcrumbPipeline` and exactly one match for the single-line token `EnsureBreadcrumbPipeline();`, which is the surviving call at line 112; that file's line count is at least 30 lines lower than the [P0-T14] baseline of 500; and a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` returns exactly one match for the single-line token `internal void EnsureBreadcrumbPipeline` and one for the single-line token `ExcludeFromCodeCoverage`. +- [x] [P2-T3] Relocate the breadcrumb pipeline helper OUT of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` and INTO the new file `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task runs BEFORE [P2-T14] adds the constructor argument; the reverse order puts the viewer-setup file at 501 lines in an intermediate state (R9). Move lines 132-163 verbatim — the two-part comment at 132-136, the `[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]` attribute at 137, and the member body at 138-163 — together with the blank line that separated it from its neighbour. The new file declares `internal partial class QfcItemController` in namespace `QuickFiler.Controllers` and carries only the using directives the moved code needs, which are the ones for the concrete viewer type and for the arrow-event handler; the provider is referenced by its fully qualified name in the moved code and needs no using. This is a pure relocation: no statement is added, removed or reordered. The invocation at that file's line 112 is NOT moved and NOT removed; it is an ordinary call from a sibling method of the same partial class and resolves across the partial once [P2-T4] adds the new file to the build. Acceptance: a case-sensitive search of `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` returns zero matches for the single-line token `internal void EnsureBreadcrumbPipeline` and exactly one match for the single-line token `EnsureBreadcrumbPipeline();`, which is the surviving call at line 112; that file's line count is at least 30 lines lower than the [P0-T14] baseline of 500; and a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` returns exactly one match for the single-line token `internal void EnsureBreadcrumbPipeline` and one for the single-line token `ExcludeFromCodeCoverage`. -- [ ] [P2-T4] Add one one-line self-closing Compile Include entry to `QuickFiler/QuickFiler.csproj` for the new item-controller partial, adjacent to the existing QfcItemController.ViewerSetup.cs entry at line 335. Acceptance: the project file contains exactly one new Compile Include line and the solution compiles, which is the only proof that the relocated member is still in the build. +- [x] [P2-T4] Add one one-line self-closing Compile Include entry to `QuickFiler/QuickFiler.csproj` for the new item-controller partial, adjacent to the existing QfcItemController.ViewerSetup.cs entry at line 335. Acceptance: the project file contains exactly one new Compile Include line and the solution compiles, which is the only proof that the relocated member is still in the build. -- [ ] [P2-T5] Apply the AC1 and AC2 trim inside GetAncestorChainAsync at `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` lines 34-42, after the snapshot walk and before segment mapping, so row order, banner placement and the trash pseudo-row are untouched. Read the root lazily through the [P1-T4] `ArchiveRootAccessor` property inside a try that treats any exception from the accessor as "no trim configured", which is what keeps the existing archive-root-throws behaviour intact (D2). When the accessor is null or yields a null, empty or whitespace root, return the mapped chain unchanged, which is the effective off switch. Otherwise call `ArchiveChainProjection.TryTrimBelowArchiveRoot`; on true, map and return the trimmed segments; on false, emit one error through the log4net ILog already declared at lines 17-19 AND through the [P1-T4] `ErrorSink`, and return `Array.Empty()`, which routes the Efc surface into the empty-chain single-segment fallback at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:123-131 and the QuickFiler surface into its existing scored fallback. Do not modify GetImmediateSubfoldersAsync: subfolders are below the leaf and therefore below the archive root by construction. Acceptance: `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot`, `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain`, `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty`, `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty` and `GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain` all pass, and the single retargeted provider test from [P1-T13], `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`, also passes. +- [x] [P2-T5] Apply the AC1 and AC2 trim inside GetAncestorChainAsync at `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` lines 34-42, after the snapshot walk and before segment mapping, so row order, banner placement and the trash pseudo-row are untouched. Read the root lazily through the [P1-T4] `ArchiveRootAccessor` property inside a try that treats any exception from the accessor as "no trim configured", which is what keeps the existing archive-root-throws behaviour intact (D2). When the accessor is null or yields a null, empty or whitespace root, return the mapped chain unchanged, which is the effective off switch. Otherwise call `ArchiveChainProjection.TryTrimBelowArchiveRoot`; on true, map and return the trimmed segments; on false, emit one error through the log4net ILog already declared at lines 17-19 AND through the [P1-T4] `ErrorSink`, and return `Array.Empty()`, which routes the Efc surface into the empty-chain single-segment fallback at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:123-131 and the QuickFiler surface into its existing scored fallback. Do not modify GetImmediateSubfoldersAsync: subfolders are below the leaf and therefore below the archive root by construction. Acceptance: `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot`, `GetAncestorChainAsync_WithoutRootAccessor_ReturnsTheUntrimmedChain`, `GetAncestorChainAsync_ChainMissesArchiveRoot_LogsErrorAndReturnsEmpty`, `GetAncestorChainAsync_LeafIsTheArchiveRoot_LogsErrorAndReturnsEmpty` and `GetAncestorChainAsync_RootAccessorThrows_DoesNotThrowAndReturnsTheUntrimmedChain` all pass, and the single retargeted provider test from [P1-T13], `GetAncestorChainAsync_WithRootAccessor_ReturnsSegmentsBelowTheArchiveRoot_HappyPath`, also passes. -- [ ] [P2-T6] Implement the AC7 log gate and absence classification in `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`. Change ResolveByUniqueSuffix at lines 90-114 from `private static` to a private instance member so it can reach per-instance state, and add the two `ConcurrentDictionary` fields D6 requires, both with `StringComparer.OrdinalIgnoreCase`: a reported-labels set that gates the existing `logger.Error` emission at lines 108-112 through `TryAdd`, so a label already reported by this provider instance emits nothing further, and an absent-labels set that gains the requested path when the candidate count is zero. Keep the message's two causes distinguishable exactly as they are today. Route every emission through the `ErrorSink` as well as through log4net. In ResolveLeafKeyAsync, remove the requested path from the absent-labels set on BOTH success routes — the exact-path match that returns at lines 74-77 and a successful unique-suffix match — so a label that becomes resolvable after a snapshot refresh is no longer reported absent. Replace the [P1-T4] `IsAbsentLabel` seam with a lookup against the absent-labels set. A bare `HashSet` is prohibited here because ResolveLeafKeyAsync awaits AcquireSnapshotAsync and its continuations are not guaranteed to run on one thread; a static set is prohibited because it is process-wide mutable state shared across viewers and across test methods in one assembly. Acceptance: `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` all pass; `ResolveLeafKeyAsync_ArchiveRelativeStem_ResolvesToUniqueSuffixMatchNode` in UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs still passes; and a case-sensitive search of the provider file returns zero matches for the single-line literal `Issue #799: the absence report body is supplied by`. +- [x] [P2-T6] Implement the AC7 log gate and absence classification in `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs`. Change ResolveByUniqueSuffix at lines 90-114 from `private static` to a private instance member so it can reach per-instance state, and add the two `ConcurrentDictionary` fields D6 requires, both with `StringComparer.OrdinalIgnoreCase`: a reported-labels set that gates the existing `logger.Error` emission at lines 108-112 through `TryAdd`, so a label already reported by this provider instance emits nothing further, and an absent-labels set that gains the requested path when the candidate count is zero. Keep the message's two causes distinguishable exactly as they are today. Route every emission through the `ErrorSink` as well as through log4net. In ResolveLeafKeyAsync, remove the requested path from the absent-labels set on BOTH success routes — the exact-path match that returns at lines 74-77 and a successful unique-suffix match — so a label that becomes resolvable after a snapshot refresh is no longer reported absent. Replace the [P1-T4] `IsAbsentLabel` seam with a lookup against the absent-labels set. A bare `HashSet` is prohibited here because ResolveLeafKeyAsync awaits AcquireSnapshotAsync and its continuations are not guaranteed to run on one thread; a static set is prohibited because it is process-wide mutable state shared across viewers and across test methods in one assembly. Acceptance: `ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndReportsAbsence`, `ResolveLeafKeyAsync_AmbiguousLabel_EmitsOneErrorAndDoesNotReportAbsence` and `ResolveLeafKeyAsync_AbsentThenResolvableLabel_ClearsTheAbsenceReport` all pass; `ResolveLeafKeyAsync_ArchiveRelativeStem_ResolvesToUniqueSuffixMatchNode` in UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs still passes; and a case-sensitive search of the provider file returns zero matches for the single-line literal `Issue #799: the absence report body is supplied by`. -- [ ] [P2-T7] Replace the ProjectSuggestionPath body at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 848-861 with a delegation to `ArchiveStemProjection.ToDisplayStem`, passing the archive root read defensively from the globals so the existing `_globals is null` identity guard is preserved without a separate early return. This eliminates the empty-root one-separator strip that AC4 names, because the contract returns false for a whitespace-only root at ArchiveStemContract.cs:113. Write the delegation as `ArchiveStemProjection.ToDisplayStem(folderPath, root)!`, with the null-forgiving operator and a one-line comment giving its reason: this file opens with `#nullable enable` at line 1 and ProjectSuggestionPath declares a non-nullable `string` return and a non-nullable `string folderPath` parameter at UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:848, while [P1-T1] declares `ToDisplayStem` with a `string?` return; `ToDisplayStem` returns null only when its own `folderPath` argument is null, which this call site's non-nullable parameter excludes, so the operator is sound and it is required because the unsuppressed form is CS8603 and the [P3-T4] gate promotes it to a build error. Do not change ProjectSuggestionPath's signature: it is called from AddSuggestions at line 810 and AddSuggestionRows at line 842 and widening its return would propagate CS8600 into both. Both call sites are display paths and are unchanged: AddSuggestions at line 810 and AddSuggestionRows at line 842. Acceptance: `Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths` and `Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs both still pass, and `AddSuggestions_WhenSuggestionsExist_AppendsHeaderAndTopSuggestions` still passes. +- [x] [P2-T7] Replace the ProjectSuggestionPath body at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 848-861 with a delegation to `ArchiveStemProjection.ToDisplayStem`, passing the archive root read defensively from the globals so the existing `_globals is null` identity guard is preserved without a separate early return. This eliminates the empty-root one-separator strip that AC4 names, because the contract returns false for a whitespace-only root at ArchiveStemContract.cs:113. Write the delegation as `ArchiveStemProjection.ToDisplayStem(folderPath, root)!`, with the null-forgiving operator and a one-line comment giving its reason: this file opens with `#nullable enable` at line 1 and ProjectSuggestionPath declares a non-nullable `string` return and a non-nullable `string folderPath` parameter at UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs:848, while [P1-T1] declares `ToDisplayStem` with a `string?` return; `ToDisplayStem` returns null only when its own `folderPath` argument is null, which this call site's non-nullable parameter excludes, so the operator is sound and it is required because the unsuppressed form is CS8603 and the [P3-T4] gate promotes it to a build error. Do not change ProjectSuggestionPath's signature: it is called from AddSuggestions at line 810 and AddSuggestionRows at line 842 and widening its return would propagate CS8600 into both. Both call sites are display paths and are unchanged: AddSuggestions at line 810 and AddSuggestionRows at line 842. Acceptance: `Issue609_FolderPredictor_ProjectsOnlyInRootFullSuggestionPaths` and `Issue609_FolderPredictor_ProjectsCaseVariantInRootFullSuggestionPath` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs both still pass, and `AddSuggestions_WhenSuggestionsExist_AppendsHeaderAndTopSuggestions` still passes. -- [ ] [P2-T8] Project each recent entry through the same helper at both AC5 sites in `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`: the string append at line 793 inside AddRecents, and the row-model mirror at line 879 inside AddRecentRows. Both must be projected, because the XML doc at lines 233-242 asserts that the string list and the row list are text-identical and the row list is the one the breadcrumb surfaces actually consume, so projecting only one would break a documented contract. Write both projections as `ArchiveStemProjection.ToDisplayStem(recent, root)!`, with the null-forgiving operator and a one-line comment giving its reason, for the same reason [P2-T7] carries one. This file opens with `#nullable enable` at line 1 and [P1-T1] declares `ToDisplayStem` with a `string?` return, while both consuming positions are non-nullable: `List.AddRange` at the string-append site takes an `IEnumerable`, so an unsuppressed `Select` over the projection is CS8620, and the `FolderRow` constructor at UtilitiesCS/OutlookObjects/Folder/FolderRow.cs:42 declares a non-nullable `string text` first parameter, so an unsuppressed argument at the row site is CS8604; the [P3-T4] gate promotes either to a build error. The operator is sound rather than defensive because `ToDisplayStem` returns null only when its own `folderPath` argument is null, and every entry of `_globals.AF.RecentsList` is a non-nullable `string` — the member is declared `SloLinkedList RecentsList { get; }` at UtilitiesCS/Interfaces/IGlobals/IAppAutoFileObjects.cs:22. The two comment lines are inside the D11 budget: FolderPredictor.cs has at least eight lines of derived slack against its 1003-line no-growth budget. Acceptance: all four `[TestMethod]` tests in `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` pass, and `AddRecents_WhenRecentsExist_AppendsHeaderAndEntries`, `FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents` and `FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds` all still pass, because their fixtures use already-relative recents for which the projection is the identity. +- [x] [P2-T8] Project each recent entry through the same helper at both AC5 sites in `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs`: the string append at line 793 inside AddRecents, and the row-model mirror at line 879 inside AddRecentRows. Both must be projected, because the XML doc at lines 233-242 asserts that the string list and the row list are text-identical and the row list is the one the breadcrumb surfaces actually consume, so projecting only one would break a documented contract. Write both projections as `ArchiveStemProjection.ToDisplayStem(recent, root)!`, with the null-forgiving operator and a one-line comment giving its reason, for the same reason [P2-T7] carries one. This file opens with `#nullable enable` at line 1 and [P1-T1] declares `ToDisplayStem` with a `string?` return, while both consuming positions are non-nullable: `List.AddRange` at the string-append site takes an `IEnumerable`, so an unsuppressed `Select` over the projection is CS8620, and the `FolderRow` constructor at UtilitiesCS/OutlookObjects/Folder/FolderRow.cs:42 declares a non-nullable `string text` first parameter, so an unsuppressed argument at the row site is CS8604; the [P3-T4] gate promotes either to a build error. The operator is sound rather than defensive because `ToDisplayStem` returns null only when its own `folderPath` argument is null, and every entry of `_globals.AF.RecentsList` is a non-nullable `string` — the member is declared `SloLinkedList RecentsList { get; }` at UtilitiesCS/Interfaces/IGlobals/IAppAutoFileObjects.cs:22. The two comment lines are inside the D11 budget: FolderPredictor.cs has at least eight lines of derived slack against its 1003-line no-growth budget. Acceptance: all four `[TestMethod]` tests in `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` pass, and `AddRecents_WhenRecentsExist_AppendsHeaderAndEntries`, `FolderArray_WhenSuggestionsAndRecentsExist_ReturnsSuggestionsThenRecents` and `FolderRowArray_WithSuggestionsAndRecents_MatchesFolderArrayTextAndTagsKinds` all still pass, because their fixtures use already-relative recents for which the projection is the identity. -- [ ] [P2-T9] Rewrite the include-children TRUE branch of GetOlSubpath at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 955-965 as a verified prefix removal through `ArchiveStemContract.TryMakeArchiveRelative`, falling back to the input path when the contract returns false, so a path that does not start with the ancestor yields a diagnosable value instead of a garbage substring and a path no longer than the ancestor no longer throws ArgumentOutOfRangeException. The contract's parameter is only NAMED archiveRoot and is root-agnostic, which is what lets it serve this site, where the ancestor is a search root supplied by the caller. Leave the include-children FALSE branch at lines 966-970 exactly as it is: it computes a leaf name, which the contract does not do, and converting it would change a different function. Acceptance: `GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs still passes, and both GetRelevantOlPathPortion tests in ToDoModel.Test, directory Email Utilities, file FolderHandlerTests_Written.cs still pass; those two ToDoModel.Test assertions are not executed by any Phase 2 command in this plan, and the earliest scheduled run that includes that assembly is [P3-T5], whose `NEWLY-FAILING: NONE` condition is where a regression in them surfaces. +- [x] [P2-T9] Rewrite the include-children TRUE branch of GetOlSubpath at `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` lines 955-965 as a verified prefix removal through `ArchiveStemContract.TryMakeArchiveRelative`, falling back to the input path when the contract returns false, so a path that does not start with the ancestor yields a diagnosable value instead of a garbage substring and a path no longer than the ancestor no longer throws ArgumentOutOfRangeException. The contract's parameter is only NAMED archiveRoot and is root-agnostic, which is what lets it serve this site, where the ancestor is a search root supplied by the caller. Leave the include-children FALSE branch at lines 966-970 exactly as it is: it computes a leaf name, which the contract does not do, and converting it would change a different function. Acceptance: `GetOlSubpath_WhenAncestorEndsWithSlashOrChildrenExcluded_ReturnsExpectedSegment` in UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs still passes, and both GetRelevantOlPathPortion tests in ToDoModel.Test, directory Email Utilities, file FolderHandlerTests_Written.cs still pass; those two ToDoModel.Test assertions are not executed by any Phase 2 command in this plan, and the earliest scheduled run that includes that assembly is [P3-T5], whose `NEWLY-FAILING: NONE` condition is where a regression in them surfaces. -- [ ] [P2-T10] Replace the ProjectPredeterminedFolder body at `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` lines 272-285 with a one-line delegation to `ArchiveStemProjection.ToDisplayStem`, retaining the member itself so its existing test keeps a target, and rewrite the XML doc at lines 252-271 so the two paragraphs describing the empty-root divergence are replaced by a statement that both members now share one projection and that the empty-root strip was removed by AC4. Also rewrite the duplication-rationale comment at lines 223-230, whose stated reason — that FolderPredictor.ProjectSuggestionPath is private and lives under UtilitiesCS, which the earlier change could not modify — becomes false the moment the shared helper is public. Acceptance: `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` passes as retargeted by [P1-T14]; a case-sensitive search of the file returns zero matches for the single-line literal `is private and lives under UtilitiesCS`; and the file is at or below its [P0-T14] baseline of 312 lines. +- [x] [P2-T10] Replace the ProjectPredeterminedFolder body at `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` lines 272-285 with a one-line delegation to `ArchiveStemProjection.ToDisplayStem`, retaining the member itself so its existing test keeps a target, and rewrite the XML doc at lines 252-271 so the two paragraphs describing the empty-root divergence are replaced by a statement that both members now share one projection and that the empty-root strip was removed by AC4. Also rewrite the duplication-rationale comment at lines 223-230, whose stated reason — that FolderPredictor.ProjectSuggestionPath is private and lives under UtilitiesCS, which the earlier change could not modify — becomes false the moment the shared helper is public. Acceptance: `ProjectPredeterminedFolder_BoundaryCases_MatchFolderPredictorProjection` passes as retargeted by [P1-T14]; a case-sensitive search of the file returns zero matches for the single-line literal `is private and lives under UtilitiesCS`; and the file is at or below its [P0-T14] baseline of 312 lines. -- [ ] [P2-T11] Project the score paths inside the internal four-argument BindRowsAsync at `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` lines 92-150, immediately before the `_builder.BuildRows` call at lines 132-136, using the bound root the method already normalizes at lines 107-109. Build a new score list that contains every original score UNCHANGED and, additionally, one projected `FolderScore` carrying `ArchiveStemProjection.ToDisplayStem(score.FolderPath, _boundRoot)!` with the same score and probability whenever that projection differs from the original path under an ordinal comparison. The null-forgiving operator carries a one-line comment giving its reason: this file opens with `#nullable enable` at line 1, [P1-T1] declares `ToDisplayStem` with a `string?` return, and `ToDisplayStem` returns null only when its own `folderPath` argument is null, which the projection loop excludes by skipping any score whose `FolderPath` is null before calling; the unsuppressed form is CS8600 or CS8604 at the `FolderScore` construction and the [P3-T4] gate promotes either to a build error. The addition is what makes it safe: substitution would fix the stem-presented case and silently break the rooted-presented case (D7), and the probability index at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:224 assigns through the indexer, so duplicate keys are tolerated rather than throwing. The public three-argument overload at lines 75-82 forwards an empty root, so the projection is the identity for every caller of that overload and no existing behaviour changes there. Do not modify the row builder and do not change any public signature; re-keying the join was considered and rejected because BuildRows takes only a string list and a score sequence, so there is no correlating identity to key on without changing a public signature and every test that calls it. Acceptance: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage`, `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` and `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` all pass, and `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` and `Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` in QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs both still pass. +- [x] [P2-T11] Project the score paths inside the internal four-argument BindRowsAsync at `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` lines 92-150, immediately before the `_builder.BuildRows` call at lines 132-136, using the bound root the method already normalizes at lines 107-109. Build a new score list that contains every original score UNCHANGED and, additionally, one projected `FolderScore` carrying `ArchiveStemProjection.ToDisplayStem(score.FolderPath, _boundRoot)!` with the same score and probability whenever that projection differs from the original path under an ordinal comparison. The null-forgiving operator carries a one-line comment giving its reason: this file opens with `#nullable enable` at line 1, [P1-T1] declares `ToDisplayStem` with a `string?` return, and `ToDisplayStem` returns null only when its own `folderPath` argument is null, which the projection loop excludes by skipping any score whose `FolderPath` is null before calling; the unsuppressed form is CS8600 or CS8604 at the `FolderScore` construction and the [P3-T4] gate promotes either to a build error. The addition is what makes it safe: substitution would fix the stem-presented case and silently break the rooted-presented case (D7), and the probability index at UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs:224 assigns through the indexer, so duplicate keys are tolerated rather than throwing. The public three-argument overload at lines 75-82 forwards an empty root, so the projection is the identity for every caller of that overload and no existing behaviour changes there. Do not modify the row builder and do not change any public signature; re-keying the join was considered and rejected because BuildRows takes only a string list and a score sequence, so there is no correlating identity to key on without changing a public signature and every test that calls it. Acceptance: `BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage`, `BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage`, `BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged` and `BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey` all pass, and `Issue439RootedTargetUsesOriginalPathForProviderLookupCaseInsensitively` and `Issue439ArchiveRelativeRowsRenderLineagePreserveFilingTargetAndProbability` in QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs both still pass. -- [ ] [P2-T12] Apply the AC7 zero-candidate row suppression on the Efc surface only, per the escalation branch D5 records, inside the same internal BindRowsAsync in `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`. Add a private readonly `IFolderLabelAbsenceReport` field assigned in the existing constructor body at lines 48-56 from `provider as IFolderLabelAbsenceReport`, which changes no constructor signature and therefore breaks no existing test; a `Mock` is not an `IFolderLabelAbsenceReport`, so the field is null and suppression is inert in every existing router test (D4). In the chain loop at lines 110-130, when a suggestion row's hierarchy path is non-null, its fetched chain is null, and the absence report says that hierarchy path is an absent label, record the presented text in a suppression set; a null chain arising from cancellation or from a provider fault is NOT suppressed, because those rows are not known-absent. Derive the retained presented-row list from the suppression set and pass that SAME list to both `_builder.BuildRows` at lines 132-136 and `AttachSegmentKeys` at line 137, because AttachSegmentKeys indexes the presented rows by row index at line 176 and would mis-align against an unfiltered list. Log the suppressed count at DEBUG through the `log` field declared at lines 21-23. When nothing is suppressed, pass the original list unchanged so the common path allocates nothing. Acceptance: the solution compiles; every `[TestMethod]` in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` passes, including `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned`, which takes the true arm of this task's suppression predicate, and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed`, which takes its false arm and pins the zero-candidate restriction at the router boundary; and all ten tests of the partial class `BreadcrumbBridgeRouterIssue439Tests`, across both its files, still pass, which is the observable proof that suppression is inert behind a provider mock that does not implement the absence report. +- [x] [P2-T12] Apply the AC7 zero-candidate row suppression on the Efc surface only, per the escalation branch D5 records, inside the same internal BindRowsAsync in `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`. Add a private readonly `IFolderLabelAbsenceReport` field assigned in the existing constructor body at lines 48-56 from `provider as IFolderLabelAbsenceReport`, which changes no constructor signature and therefore breaks no existing test; a `Mock` is not an `IFolderLabelAbsenceReport`, so the field is null and suppression is inert in every existing router test (D4). In the chain loop at lines 110-130, when a suggestion row's hierarchy path is non-null, its fetched chain is null, and the absence report says that hierarchy path is an absent label, record the presented text in a suppression set; a null chain arising from cancellation or from a provider fault is NOT suppressed, because those rows are not known-absent. Derive the retained presented-row list from the suppression set and pass that SAME list to both `_builder.BuildRows` at lines 132-136 and `AttachSegmentKeys` at line 137, because AttachSegmentKeys indexes the presented rows by row index at line 176 and would mis-align against an unfiltered list. Log the suppressed count at DEBUG through the `log` field declared at lines 21-23. When nothing is suppressed, pass the original list unchanged so the common path allocates nothing. Acceptance: the solution compiles; every `[TestMethod]` in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` passes, including `BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeysAligned`, which takes the true arm of this task's suppression predicate, and `BindRowsAsync_AmbiguousLabel_IsNotSuppressed`, which takes its false arm and pins the zero-candidate restriction at the router boundary; and all ten tests of the partial class `BreadcrumbBridgeRouterIssue439Tests`, across both its files, still pass, which is the observable proof that suppression is inert behind a provider mock that does not implement the absence report. -- [ ] [P2-T13] Add the lazy root accessor argument to the provider construction at `QuickFiler/Controllers/EfcFormController.cs` lines 1053-1055, passing a delegate that reads the archive root from the existing application-globals accessor at call time rather than at construction time. The delegate form is mandatory: the archive-root property throws when the root is unresolvable, this construction is not inside a try, and QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242 exists because of that throw (D2). Change nothing else in this file; the raw score read at lines 1115-1117 and the four-argument router call at line 1118 stay exactly as they are, because AC6 is delivered in the router (D7) and this file cannot absorb growth. Acceptance: the solution compiles; `BindBreadcrumbRowsAsync_WhenArchiveRootThrows_ReportsOnceAndDoesNotThrow` still passes; and the file is at or below 1322 lines, that is its [P0-T14] baseline of 1320 plus the at-most-two lines the added argument costs (D11). +- [x] [P2-T13] Add the lazy root accessor argument to the provider construction at `QuickFiler/Controllers/EfcFormController.cs` lines 1053-1055, passing a delegate that reads the archive root from the existing application-globals accessor at call time rather than at construction time. The delegate form is mandatory: the archive-root property throws when the root is unresolvable, this construction is not inside a try, and QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs:242 exists because of that throw (D2). Change nothing else in this file; the raw score read at lines 1115-1117 and the four-argument router call at line 1118 stay exactly as they are, because AC6 is delivered in the router (D7) and this file cannot absorb growth. Acceptance: the solution compiles; `BindBreadcrumbRowsAsync_WhenArchiveRootThrows_ReportsOnceAndDoesNotThrow` still passes; and the file is at or below 1322 lines, that is its [P0-T14] baseline of 1320 plus the at-most-two lines the added argument costs (D11). -- [ ] [P2-T14] Add the same lazy root accessor argument to the provider construction inside the relocated helper in `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task must run after [P2-T3] (R9). Change nothing in `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` in this task. Acceptance: the solution compiles; a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` finds the single-line token `ArchiveRootPath`; and `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is at or below 500 lines. +- [x] [P2-T14] Add the same lazy root accessor argument to the provider construction inside the relocated helper in `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs`. This task must run after [P2-T3] (R9). Change nothing in `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` in this task. Acceptance: the solution compiles; a case-sensitive search of `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` finds the single-line token `ArchiveRootPath`; and `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` is at or below 500 lines. -- [ ] [P2-T15] Build the solution and record `/evidence/regression-testing/p2-t15-build.md`. Acceptance: `EXIT_CODE: 0`. +- [x] [P2-T15] Build the solution and record `/evidence/regression-testing/p2-t15-build.md`. Acceptance: `EXIT_CODE: 0`. ```powershell msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" ``` -- [ ] [P2-T16] Run every test named in the [P1-T18] inventory and record `/evidence/regression-testing/p2-t16-pass-after.md`. Acceptance: the artifact records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, both `0`, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema; `FAILED-UT: 0` and `FAILED-QFT: 0`, each read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run; and, for every test named in the [P1-T18] inventory, a `PASS-AFTER: ` line derived from the TRX, with the count of those lines equal to the [P1-T18] inventory count. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. The two runs' own totals are recorded separately as `P2-T16-TOTAL-PASSED:` and `P2-T16-TOTAL-RUN:` and are NOT asserted against the inventory count, because the filters select whole classes and therefore also run tests that were already green at the end of Phase 1. +- [x] [P2-T16] Run every test named in the [P1-T18] inventory and record `/evidence/regression-testing/p2-t16-pass-after.md`. Acceptance: the artifact records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, both `0`, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema; `FAILED-UT: 0` and `FAILED-QFT: 0`, each read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run; and, for every test named in the [P1-T18] inventory, a `PASS-AFTER: ` line derived from the TRX, with the count of those lines equal to the [P1-T18] inventory count. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. The two runs' own totals are recorded separately as `P2-T16-TOTAL-PASSED:` and `P2-T16-TOTAL-RUN:` and are NOT asserted against the inventory count, because the filters select whole classes and therefore also run tests that were already green at the end of Phase 1. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -646,7 +646,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p2-t16-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:FullyQualifiedName~BreadcrumbBridgeRouterScoreJoinTests|FullyQualifiedName~QfcItemController_FolderHandlingTests|FullyQualifiedName~BreadcrumbBridgeRouterIssue439Tests' ``` -- [ ] [P2-T17] Run the whole UtilitiesCS.Test and QuickFiler.Test assemblies and record `/evidence/regression-testing/p2-t17-suites.md` with the derived lines `POST-UT-TOTAL:`, `POST-UT-PASSED:`, `POST-UT-FAILED:`, `POST-QFT-TOTAL:`, `POST-QFT-PASSED:`, `POST-QFT-FAILED:` and a `NEWLY-FAILING:` line listing every test failing here that was not failing in the [P0-T11] baseline. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: the artifact records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema; each `POST-*-FAILED` value is read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run; `NEWLY-FAILING: NONE`; `POST-UT-FAILED` is less than or equal to `BASELINE-UT-FAILED`; and `POST-QFT-FAILED` is less than or equal to `BASELINE-QFT-FAILED`. +- [x] [P2-T17] Run the whole UtilitiesCS.Test and QuickFiler.Test assemblies and record `/evidence/regression-testing/p2-t17-suites.md` with the derived lines `POST-UT-TOTAL:`, `POST-UT-PASSED:`, `POST-UT-FAILED:`, `POST-QFT-TOTAL:`, `POST-QFT-PASSED:`, `POST-QFT-FAILED:` and a `NEWLY-FAILING:` line listing every test failing here that was not failing in the [P0-T11] baseline. Where a results directory holds more than one TRX because a task was re-run, the most recently modified TRX is the one read, and the artifact records which file it was by name reduced per R3. Acceptance: the artifact records the two derived lines `EXIT-CODE-UT:` and `EXIT-CODE-QFT:`, one per invocation, together with a single `EXIT_CODE:` field equal to the larger of the two so the artifact satisfies the evidence schema; each `POST-*-FAILED` value is read from its run's TRX `ResultSummary/Counters` `failed` attribute and NOT from the console, because vstest prints no `Failed:` line at all on a fully passing run; `NEWLY-FAILING: NONE`; `POST-UT-FAILED` is less than or equal to `BASELINE-UT-FAILED`; and `POST-QFT-FAILED` is less than or equal to `BASELINE-QFT-FAILED`. ```powershell $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" @@ -655,7 +655,7 @@ $vstest = & $vswhere -latest -products * -find 'Common7\IDE\Extensions\TestPlatf & $vstest QuickFiler.Test\bin\Debug\QuickFiler.Test.dll '/Settings:scripts\vscode\TaskMaster.cli.runsettings' '/InIsolation' '/Logger:trx' '/ResultsDirectory:TestResults\799-p2-t17-qft' '/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None' '/TestCaseFilter:TestCategory!=LiveOutlook' ``` -- [ ] [P2-T18] Record the pre-format line count of every file this plan has edited or created into `/evidence/qa-gates/p2-t18-sizes-interim.md`, one ` = ` line per file alongside its [P0-T14] baseline count, keeping the four project files under the same `PROJECT-FILE (exempt):` heading [P0-T14] uses (R8). Acceptance: every listed `.cs` count satisfies its D11 budget; the exempt project-file counts are recorded but not asserted against the ceiling; and any `.cs` file within ten lines of its budget is named explicitly with its remaining headroom. +- [x] [P2-T18] Record the pre-format line count of every file this plan has edited or created into `/evidence/qa-gates/p2-t18-sizes-interim.md`, one ` = ` line per file alongside its [P0-T14] baseline count, keeping the four project files under the same `PROJECT-FILE (exempt):` heading [P0-T14] uses (R8). Acceptance: every listed `.cs` count satisfies its D11 budget; the exempt project-file counts are recorded but not asserted against the ceiling; and any `.cs` file within ten lines of its budget is named explicitly with its remaining headroom. --- From 7db935b791cf81e0f6df00fef6ef084a8a7a2b4c Mon Sep 17 00:00:00 2001 From: Dan Moisan Date: Mon, 7 Sep 2026 08:14:02 -0400 Subject: [PATCH 11/13] qa(799): Phase 3 final QA loop, coverage and acceptance-criteria closure Close out the atomic plan for issue #799 with the full C# toolchain loop, coverage comparison and acceptance-criteria check-off. Toolchain loop closed clean in one uninterrupted pass, in the mandated order: - CSharpier format: exit 0, "Formatted 1601 files in 8171ms.". Nine files touched, five with a content change, all inside the plan's Write Set, so nothing was reverted and the scope boundary is unchanged. - CSharpier check: exit 0, "Checked 1601 files in 6748ms.", no drift. The count is the 1593-file baseline plus exactly the eight new .cs files. - Analyzer gate (/t:Rebuild, EnableNETAnalyzers, EnforceCodeStyleInBuild): exit 0, 0 Warning(s), 0 Error(s), matching the baseline. - Nullable gate (/t:Rebuild, TreatWarningsAsErrors): exit 0, 0 Warning(s), 0 Error(s), zero CS86 diagnostics, matching the baseline. - Nine-assembly run under dotnet-coverage: exit 0, 7085 tests, 7085 passed, 0 failed, NEWLY-FAILING: NONE against a 7048-test baseline. Coverage, on the pinned comparability index applied identically to both documents with PACKAGES_MATCHED 9 on each side: lines 112855/133485 (84.55 percent) to 113143/133765 (84.58 percent); branches 26642/33624 (79.24 percent) to 26746/33736 (79.28 percent). The denominator grew, so the percentages are the comparison used. Neither decreased. Of 315 changed production lines, 0 have lower post-change hits than their baseline. Both new production types reach 100 percent line and branch coverage. Size budgets after the final format: ViewerSetup 467/500, EfcFormController 1321/1322, FolderPredictor 1002/1003, every other .cs at or below 500. The formatter did not touch the two files carrying one line of headroom. Scope: the changed source set under '*.cs' '*.csproj' is exactly the twenty Write Set paths. All six sibling-owned files and both issue-439 test files were queried individually and carry no hunk. All eight acceptance criteria AC1 through AC8 are checked off in spec.md. spec.md Status is now Implemented and records the four deviations from its own prose, with the AC7 escalation (row suppression on the Efc surface only, logging on both) written before the AC7 box was checked. issue.md records the outcome and states that this is a specification change superseding #439 rather than a regression fix against it, with #439's filing-target and score-key constraint carried forward as AC3. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MS8fb2wX1Sx1bG67LAr41e --- .../BreadcrumbBridgeRouterScoreJoinTests.cs | 76 ++- .../FolderPredictorRecentsProjectionTests.cs | 3 +- ...OutlookFolderHierarchyProviderTrimTests.cs | 4 +- .../Folder/ArchiveChainProjection.cs | 3 +- .../Folder/OutlookFolderHierarchyProvider.cs | 4 +- .../issue-799.2026-09-07T08-35.md | 94 ++++ .../evidence/qa-gates/p3-t1-format.md | 121 ++++ .../evidence/qa-gates/p3-t10-sizes.md | 115 ++++ .../evidence/qa-gates/p3-t11-scope.md | 151 +++++ .../qa-gates/p3-t12-ac8-verification.md | 138 +++++ .../evidence/qa-gates/p3-t2-format-check.md | 46 ++ .../evidence/qa-gates/p3-t20-ac8-assets.md | 40 ++ .../evidence/qa-gates/p3-t23-ac-summary.md | 95 ++++ .../evidence/qa-gates/p3-t3-analyzers.md | 46 ++ .../evidence/qa-gates/p3-t4-nullable.md | 59 ++ .../evidence/qa-gates/p3-t5-tests-coverage.md | 120 ++++ .../evidence/qa-gates/p3-t6-loop.md | 60 ++ .../evidence/qa-gates/p3-t7-changed-lines.md | 528 ++++++++++++++++++ .../evidence/qa-gates/p3-t8-coverage-delta.md | 96 ++++ .../qa-gates/p3-t9-new-type-coverage.md | 59 ++ .../issue.md | 63 +++ .../plan.2026-09-06T22-01.md | 46 +- .../spec.md | 86 ++- 23 files changed, 1997 insertions(+), 56 deletions(-) create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/issue-updates/issue-799.2026-09-07T08-35.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t1-format.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t10-sizes.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t11-scope.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t12-ac8-verification.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t2-format-check.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t20-ac8-assets.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t23-ac-summary.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t3-analyzers.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t4-nullable.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t5-tests-coverage.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t6-loop.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t7-changed-lines.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t8-coverage-delta.md create mode 100644 docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t9-new-type-coverage.md diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs index 90558860c..4a39b3146 100644 --- a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs @@ -44,8 +44,12 @@ public void BindRowsAsync_RootedScoreAndRelativeRow_RendersThePercentage() var provider = new Mock(MockBehavior.Strict); var host = StrictHost(documents); FolderTreeNodeKey key = Key(RootedTarget); - SetupChain(provider, RootedTarget, key, TwoSegmentChain(ClientsPath, "Clients", - RootedTarget, "North")); + SetupChain( + provider, + RootedTarget, + key, + TwoSegmentChain(ClientsPath, "Clients", RootedTarget, "North") + ); var router = RouterOver(provider, host); // Act: presented row is the stem; the score carries the rooted path. @@ -76,8 +80,12 @@ public void BindRowsAsync_RootedScoreAndRootedRow_StillRendersThePercentage() var provider = new Mock(MockBehavior.Strict); var host = StrictHost(documents); FolderTreeNodeKey key = Key(RootedTarget); - SetupChain(provider, RootedTarget, key, TwoSegmentChain(ClientsPath, "Clients", - RootedTarget, "North")); + SetupChain( + provider, + RootedTarget, + key, + TwoSegmentChain(ClientsPath, "Clients", RootedTarget, "North") + ); var router = RouterOver(provider, host); // Act: presented row and score both carry the rooted path. @@ -108,8 +116,12 @@ public void BindRowsAsync_EmptyBoundRoot_LeavesTheJoinUnchanged() var provider = new Mock(MockBehavior.Strict); var host = StrictHost(documents); FolderTreeNodeKey key = Key(RelativeTarget); - SetupChain(provider, RelativeTarget, key, TwoSegmentChain(@"Clients", "Clients", - RelativeTarget, "North")); + SetupChain( + provider, + RelativeTarget, + key, + TwoSegmentChain(@"Clients", "Clients", RelativeTarget, "North") + ); var router = RouterOver(provider, host); // Act: the public overload, which supplies no archive root at all. @@ -140,8 +152,12 @@ public void BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey() var provider = new Mock(MockBehavior.Strict); var host = StrictHost(documents); FolderTreeNodeKey key = Key(RootedTarget); - SetupChain(provider, RootedTarget, key, TwoSegmentChain(ClientsPath, "Clients", - RootedTarget, "North")); + SetupChain( + provider, + RootedTarget, + key, + TwoSegmentChain(ClientsPath, "Clients", RootedTarget, "North") + ); var router = RouterOver(provider, host); // Act @@ -185,10 +201,18 @@ public void BindRowsAsync_MixedRowSet_RendersLineageOnFolderRowsOnly() var host = StrictHost(documents); FolderTreeNodeKey suggestionKey = Key(RootedTarget); FolderTreeNodeKey searchKey = Key(searchRooted); - SetupChain(provider, RootedTarget, suggestionKey, TwoSegmentChain(ClientsPath, - "Clients", RootedTarget, "North")); - SetupChain(provider, searchRooted, searchKey, TwoSegmentChain(@"\Archive\Search", - "Search", searchRooted, "Follow Up")); + SetupChain( + provider, + RootedTarget, + suggestionKey, + TwoSegmentChain(ClientsPath, "Clients", RootedTarget, "North") + ); + SetupChain( + provider, + searchRooted, + searchKey, + TwoSegmentChain(@"\Archive\Search", "Search", searchRooted, "Follow Up") + ); provider .Setup(p => p.ResolveLeafKeyAsync(@"\Archive\Clients\Stale", It.IsAny()) @@ -256,15 +280,25 @@ public void BindRowsAsync_ZeroCandidateLabel_SuppressesTheRowAndKeepsSegmentKeys FolderTreeNodeKey northKey = Key(RootedTarget); FolderTreeNodeKey southKey = Key(vendorsRooted); FolderTreeNodeKey vendorsKey = Key(vendorsPath); - SetupChain(provider, RootedTarget, northKey, TwoSegmentChain(ClientsPath, "Clients", - RootedTarget, "North")); - SetupChain(provider, vendorsRooted, southKey, TwoSegmentChain(vendorsPath, "Vendors", - vendorsRooted, "South")); + SetupChain( + provider, + RootedTarget, + northKey, + TwoSegmentChain(ClientsPath, "Clients", RootedTarget, "North") + ); + SetupChain( + provider, + vendorsRooted, + southKey, + TwoSegmentChain(vendorsPath, "Vendors", vendorsRooted, "South") + ); provider .Setup(p => p.ResolveLeafKeyAsync(staleRooted, It.IsAny())) .ReturnsAsync((FolderTreeNodeKey)null); provider - .Setup(p => p.GetImmediateSubfoldersAsync(vendorsKey, It.IsAny())) + .Setup(p => + p.GetImmediateSubfoldersAsync(vendorsKey, It.IsAny()) + ) .ReturnsAsync(new[] { Segment(vendorsRooted, "South", false) }); var router = RouterOver(provider, host); @@ -318,8 +352,12 @@ public void BindRowsAsync_AmbiguousLabel_IsNotSuppressed() var host = StrictHost(documents); FolderTreeNodeKey northKey = Key(RootedTarget); - SetupChain(provider, RootedTarget, northKey, TwoSegmentChain(ClientsPath, "Clients", - RootedTarget, "North")); + SetupChain( + provider, + RootedTarget, + northKey, + TwoSegmentChain(ClientsPath, "Clients", RootedTarget, "North") + ); provider .Setup(p => p.ResolveLeafKeyAsync(ambiguousRooted, It.IsAny())) .ReturnsAsync((FolderTreeNodeKey)null); diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs index 34576beca..cd93bf1e5 100644 --- a/UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs +++ b/UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs @@ -184,8 +184,7 @@ IDictionary foldersByName ) { var folders = new Mock(); - var enumerableItems = - foldersByName?.Values?.ToArray() ?? Array.Empty(); + var enumerableItems = foldersByName?.Values?.ToArray() ?? Array.Empty(); var collection = new ArrayList(enumerableItems); folders diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs index d9461ac0e..11fab24e5 100644 --- a/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs +++ b/UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs @@ -176,7 +176,9 @@ public async Task ResolveLeafKeyAsync_SameAbsentLabelTwice_EmitsOneErrorAndRepor await provider.ResolveLeafKeyAsync(MissingStem, CancellationToken.None); // Assert - errors.Should().ContainSingle("the gate is once per label per session, not per render"); + errors + .Should() + .ContainSingle("the gate is once per label per session, not per render"); provider.IsAbsentLabel(MissingStem).Should().BeTrue(); } diff --git a/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs b/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs index a73318eb7..6744074c0 100644 --- a/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs +++ b/UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs @@ -63,7 +63,8 @@ out IReadOnlyList trimmed chain[index].FolderPath, archiveRoot, out var stem - ) && stem.Length == 0; + ) + && stem.Length == 0; if (!isRootNode) { diff --git a/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs b/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs index 240c418c1..bfb2e351d 100644 --- a/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs +++ b/UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs @@ -118,7 +118,9 @@ CancellationToken cancellationToken return mapped; } - if (ArchiveChainProjection.TryTrimBelowArchiveRoot(mapped, archiveRoot, out var trimmed)) + if ( + ArchiveChainProjection.TryTrimBelowArchiveRoot(mapped, archiveRoot, out var trimmed) + ) { return trimmed; } diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/issue-updates/issue-799.2026-09-07T08-35.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/issue-updates/issue-799.2026-09-07T08-35.md new file mode 100644 index 000000000..b525da836 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/issue-updates/issue-799.2026-09-07T08-35.md @@ -0,0 +1,94 @@ +# Issue #799 outcome update — local mirror + +Timestamp: 2026-09-07T08-35 + +PostedAs: unknown + +POSTING BLOCKED — this delegation is scoped to the item worktree and explicitly excludes pushing, opening a pull +request and merging; GitHub interaction for this item is handled after review. The text below was written to the +local feature `issue.md` under a new `## Outcome` heading appended after the existing `## Next Step` section. No +existing heading was altered, per that file's automation note. The same text appears in both places. + +Issue URL: https://github.com/drmoisan/TaskMaster/issues/799 + +Command: local file edit only; no `gh` invocation was made. + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +## Exact text written to `issue.md` + +## Outcome + +Implemented on 2026-09-07 on branch `bug/breadcrumb-lineage-below-archive-root-799`, across the three phases of +`plan.2026-09-06T22-01.md`. + +**This is a specification change superseding issue #439, not a regression fix against it.** Issue #439 delivered +full root-to-leaf ancestor lineage deliberately, and that behaviour was correct against its own acceptance +criteria. This item narrows the rendered lineage to begin at the first segment below the archive root because the +mailbox and Archive segments carry no information in a system where every filing target is under the archive root, +and because they consume most of the row width and defeat the row-distinguishability goal #439 itself set out to +serve. Nothing in #439 is being repaired. + +**#439's filing-target and score-key constraint is preserved and carried forward as AC3.** The trim removes only +LEADING segments from the rendered chain; the filing value and the score-lookup key remain the archive-relative +stem, substituted into the LEAF segment, exactly as #439 required. AC3 pins this explicitly rather than leaving it +as an incidental consequence, via the test +`BindRowsAsync_TrimmedChain_PreservesFilingTargetAndScoreKey`. All ten tests of the #439 partial class +`BreadcrumbBridgeRouterIssue439Tests`, across both its files, pass unmodified: neither file carries a hunk in this +change, because every test in them drives a strict provider mock that sits below the trim boundary. + +### What was delivered + +- AC1 and AC2: the ancestor-chain trim lives in `OutlookFolderHierarchyProvider.GetAncestorChainAsync`, the single + seam both the QuickFiler drop-down and the Efc list route through, so one change serves both surfaces. A chain + that does not pass through the archive root, or whose leaf IS the root, is logged once and returns an empty + segment list, which routes each surface into its existing fallback. +- AC3: filing target and score-lookup key remain the archive-relative stem. +- AC4: `ProjectSuggestionPath` and `ProjectPredeterminedFolder` now both delegate to the new shared + `ArchiveStemProjection.ToDisplayStem`, built on `ArchiveStemContract.TryMakeArchiveRelative`. The empty-root + one-separator strip is eliminated. Four of the seven candidate sites were converted; three were deliberately + left, with reasons recorded in the specification's decision D-A. +- AC5: recent-folder entries are projected at both sites, the string append and the row-model mirror, preserving + the documented text-parity contract between the two lists. +- AC6: the Efc router adds a projected score alongside each raw score, so an archive-rooted suggestion presented as + a stem retains its percentage and a rooted-presented row does not lose its own. +- AC7: stale labels are logged once per label per provider instance rather than once per render, on both surfaces. + Zero-candidate labels are additionally suppressed from the rendered row set on the Efc surface. +- AC8: verified as a finding, not a fix. No renderer alters a leading underscore; the reported space was a + transcription artifact and a renderer change would have been a defect. + +### Deviations from the specification's own prose + +Four, each recorded by name with its reason in `spec.md` under Rollout & Follow-up, section Outcome: AC7 row +suppression is delivered on the Efc surface only; the AC6 score projection is additive rather than substitutive; +the two #439 Efc router test files carry no hunk; and the AC7 absence classification is published through a new +small public interface rather than through a fourth member on the shared hierarchy contract. + +### Verification + +The final toolchain loop closed clean in a single pass: CSharpier format and check both exit 0 over 1601 files, +the analyzer gate and the nullable gate each exit 0 with 0 Warning(s) and 0 Error(s), and the coverage-enabled +nine-assembly run exits 0 with 7085 tests, 7085 passed, 0 failed and `NEWLY-FAILING: NONE` against a 7048-test +baseline. First-party line coverage moved from 84.55 to 84.58 percent and branch coverage from 79.24 to 79.28 +percent on the pinned comparability index; no changed line lost coverage. Both new production types reach 100 +percent line and branch coverage. Evidence is under this feature folder's `evidence/qa-gates/` and +`evidence/regression-testing/` directories. + +### Acceptance criteria + +The authoritative acceptance-criteria source for this `full-bug` item is `spec.md`, section Acceptance Criteria. +All eight criteria AC1 through AC8 are checked off there. The mirrored list under "Proposed Fix / Validation +Ideas" above is left as captured, because it is the intake record rather than the tracked criteria source. + +## End of mirrored text + +Output Summary: The issue outcome was written to the local feature `issue.md` as a new `## Outcome` section and +mirrored here verbatim. It states that this item is a specification change superseding issue #439 rather than a +regression fix against it, and that #439's filing-target and score-key constraint is preserved and carried forward +as AC3. It was not posted to GitHub, because this delegation excludes remote interaction. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t1-format.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t1-format.md new file mode 100644 index 000000000..ecb5dc025 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t1-format.md @@ -0,0 +1,121 @@ +# [P3-T1] CSharpier format pass + +Timestamp: 2026-09-07T07-49 + +Command: dotnet tool run csharpier format . +(preceded in the same block by `git add --intent-to-add -- '*.cs' '*.csproj'`, the before capture of +`git status --porcelain --untracked-files=all` and `git diff --stat `, and followed by the same two +captures again) + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +## Verbatim printed line + +``` +Formatted 1601 files in 8171ms. +``` + +`format` is a write-mode command: it rewrites tracked source and still exits 0 after rewriting, so the exit code +alone cannot distinguish a clean run from a repairing one. The distinguishing observations are the two derived +comparison lines below, captured before and after the run in the same shell. + +## Derived comparison lines + +PATH_SETS_IDENTICAL: False +DIFFSTAT_IDENTICAL: False + +Both lines are recorded with their values, which is what this task's acceptance requires. `False` on both is the +truthful observation: the formatter rewrote files, so the porcelain path set went from empty to nine entries and +the anchored diffstat changed. It is not a failure signal. The gate on formatting cleanliness is [P3-T2], whose +read-only `check` exit code is the actual pass/fail. + +## Before: `git status --porcelain --untracked-files=all` + +Empty. Phase 2 was committed at `f50fb727`, so the worktree was clean at the start of this task and the +`--intent-to-add` companion had nothing new to stage. + +## After: `git status --porcelain --untracked-files=all` + +``` + M QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs + M QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs + M UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs + M UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs + M UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs + M UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs + M UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs + M UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs + M UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs +``` + +## Scope check on the touched set + +Nine files appear in the post-run porcelain output. All nine are members of this plan's twenty-path Write Set. NO +file outside the Write Set appears, so no revert was required and the repository-wide pass did not widen the scope +boundary [P3-T11] asserts. + +Of those nine, only FIVE carry a content change. The other four were rewritten byte-identically — CSharpier +updated their modification time without changing their bytes — and `git status` reported them modified from the +refreshed stat cache before any content comparison had been made. This was measured at staging time rather than +inferred: `git diff --cached --numstat` returns zero lines for each of the four, and each file's line endings are +uniformly CRLF (CRLF count equals total LF count), so no line-ending rewrite occurred either: + +``` +STAGED_NUMSTAT_LINES=0 CRLF=41 LF_TOTAL=41 QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs +STAGED_NUMSTAT_LINES=0 CRLF=217 LF_TOTAL=217 UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs +STAGED_NUMSTAT_LINES=0 CRLF=176 LF_TOTAL=176 UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs +STAGED_NUMSTAT_LINES=0 CRLF=63 LF_TOTAL=63 UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs +``` + +Those four files were therefore already CSharpier-clean when this pass began, which is consistent with [P3-T2] +finding no drift immediately afterwards. + +## Anchored diffstat delta (before vs after), by file + +Derived by comparing the two `git diff --stat ` captures. Only five files moved; the insertion totals +went from 3305 to 3347, a net +42. + +| Path | before | after | delta | +|---|---|---|---| +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` | 425 | 463 | +38 | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` | 344 | 346 | +2 | +| `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` | 179 | 181 | +2 | +| `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` | 92 | 93 | +1 | +| `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` | 213 | 212 | -1 | +| `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` | 176 | 176 | 0, no content change | +| `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` | 217 | 217 | 0, no content change | +| `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` | 63 | 63 | 0, no content change | +| `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` | 41 | 41 | 0, no content change | + +## The three D11-budgeted files were NOT rewritten + +The [P2-T18] interim measurement left one line of headroom on two files, so a formatter rewrite of either would +have breached its budget. The formatter did not touch any of the three: + +- `QuickFiler/Controllers/EfcFormController.cs` — diffstat unchanged at `3 +/-` before and after; absent from the + rewritten set. +- `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` — diffstat unchanged at `41 +/-`; absent from the + rewritten set. +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` — diffstat unchanged at `33 --`; absent from the + rewritten set. + +[P3-T10] is the gating re-measurement and confirms the resulting counts. + +## Checked-file count + +The formatter reports 1601 files, which is the [P0-T8] baseline of 1593 plus the eight new `.cs` files this plan +adds (three production, five test). [P3-T2] records the same delta from the read-only `check`. + +Output Summary: The repository-wide CSharpier pass exited 0 and printed `Formatted 1601 files in 8171ms.`. Nine +files appear in the post-run porcelain output, every one of them inside this plan's Write Set, so nothing was +reverted and the scope boundary is unchanged; five of the nine carry a content change and the other four were +rewritten byte-identically, measured at staging time. Both required derived comparison lines are recorded: +`PATH_SETS_IDENTICAL: False` and `DIFFSTAT_IDENTICAL: False`, which is the truthful before/after observation for a +run that rewrote files. The three D11-budgeted files were not among the touched set, so the one line of headroom +[P2-T18] recorded on `EfcFormController.cs` and `FolderPredictor.cs` was not consumed by formatting. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t10-sizes.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t10-sizes.md new file mode 100644 index 000000000..4447358a7 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t10-sizes.md @@ -0,0 +1,115 @@ +# [P3-T10] Post-format line counts of every file this plan edited or created + +Timestamp: 2026-09-07T08-12 + +Command: `(Get-Content -LiteralPath ).Count` for each path below + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +CEILING: 500 (applies to *.cs only) + +This audit runs AFTER the [P3-T1] CSharpier pass, because the formatter can change line counts (R9). It supersedes +the [P2-T18] interim measurement, which was taken before the formatter ran, and it is the gating measurement. + +## Production `.cs` + +| Path | [P0-T14] baseline | [P2-T18] pre-format | [P3-T10] post-format | D11 budget | Headroom | Verdict | +|---|---|---|---|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` | NOT PRESENT | 63 | 63 | 500 | 437 | met | +| `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` | NOT PRESENT | 92 | 93 | 500 | 407 | met | +| `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` | 141 | 300 | 302 | 500 | 198 | met | +| `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` | 1003 | 1002 | 1002 | 1003 (no growth) | 1 | met | +| `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` | 312 | 295 | 295 | 500, and [P2-T10] additionally requires at or below its 312 baseline | 205 | met | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 304 | 407 | 407 | 500 | 93 | met | +| `QuickFiler/Controllers/EfcFormController.cs` | 1320 | 1321 | 1321 | 1322 | 1 | met | +| `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` | NOT PRESENT | 41 | 41 | 500 | 459 | met | +| `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` | 500 | 467 | 467 | 500 (hard) | 33 | met | + +## Test `.cs` + +| Path | [P0-T14] baseline | [P2-T18] pre-format | [P3-T10] post-format | Budget | Headroom | Verdict | +|---|---|---|---|---|---|---| +| `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` | NOT PRESENT | 176 | 176 | 500 | 324 | met | +| `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` | NOT PRESENT | 217 | 217 | 500 | 283 | met | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` | NOT PRESENT | 344 | 346 | 500 | 154 | met | +| `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` | NOT PRESENT | 213 | 212 | 500 | 288 | met | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` | NOT PRESENT | 425 | 463 | 500 | 37 | met | +| `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` | 479 | 480 | 480 | 483 ([P1-T13] +4) | 3 | met | +| `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` | 354 | 363 | 363 | 500 | 137 | met | + +## PROJECT-FILE (exempt) + +Recorded as exempt observations rather than asserted against the ceiling, per R8: the 500-line cap in +.claude/rules/general-code-change.md covers production code, test code and reusable script files and does not +reach project files, and .csharpierignore lines 9-14 record that project files are owned by Visual Studio and are +not C# source. CSharpier does not process them either, so these counts are unchanged from [P2-T18]. + +- PROJECT-FILE (exempt): `UtilitiesCS/UtilitiesCS.csproj` = 1317 (baseline 1315, +2 Compile Include) +- PROJECT-FILE (exempt): `QuickFiler/QuickFiler.csproj` = 606 (baseline 605, +1 Compile Include) +- PROJECT-FILE (exempt): `UtilitiesCS.Test/UtilitiesCS.Test.csproj` = 980 (baseline 976, +4 Compile Include) +- PROJECT-FILE (exempt): `QuickFiler.Test/QuickFiler.Test.csproj` = 530 (baseline 529, +1 Compile Include) + +## What the formatter changed + +The [P3-T1] pass moved five of the sixteen `.cs` files in this set, by a net +42 lines overall: + +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` 425 to 463 (+38) +- `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` 344 to 346 (+2) +- `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` 300 to 302 (+2) +- `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` 92 to 93 (+1) +- `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` 213 to 212 (-1) + +The two files carrying only one line of headroom — `QuickFiler/Controllers/EfcFormController.cs` at 1321 against +1322 and `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` at 1002 against 1003 — were NOT rewritten by the +formatter and are unchanged from their pre-format counts, so neither budget was breached. This was the identified +risk in this task and it did not materialise. `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` was +likewise untouched at 467. + +## Smallest remaining headroom + +SMALLEST-REMAINING-HEADROOM: 1 line, on two files. + +- `QuickFiler/Controllers/EfcFormController.cs` — 1321 against its D11 budget of 1322. +- `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` — 1002 against its D11 no-growth budget of 1003. + +Next after those: `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` at 480 against +its 483 budget (3 lines), then `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` at 463 +against 500 (37 lines). + +## The three disclosed pre-existing over-ceiling files and their budgets (D11) + +R8 and D11 disclose three files that were ALREADY over the 500-line ceiling before any change in this plan. They +are gated by a per-file budget rather than by the ceiling, because a blanket "at or below 500" assertion would be +unsatisfiable on them: + +| Path | Pre-existing count | D11 budget | Post-format count | Verdict | +|---|---|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` | 1003 | 1003, no growth | 1002 | met, and one line below the pre-existing count | +| `QuickFiler/Controllers/EfcFormController.cs` | 1320 | 1322, that is baseline plus at most two | 1321 | met | +| UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorTests.cs | 1066 | not touched by this plan | 1066, unchanged | not in the Write Set; disclosed only | + +Neither over-ceiling file is repaired here and neither grew beyond its budget. `FolderPredictor.cs` in fact ends +one line SMALLER than it began, so this change moves it toward the ceiling rather than away from it. + +## Acceptance conditions + +- `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` at or below 500: 467, met. +- `QuickFiler/Controllers/EfcFormController.cs` at or below 1322: 1321, met. +- `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` at or below 1003: 1002, met. +- Every other listed `.cs` file at or below 500: the largest is + `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` at 463, met. +- Exempt project-file counts recorded but not asserted against the ceiling: done. +- Smallest remaining headroom stated, together with the three disclosed pre-existing over-ceiling files and their + budgets: done. + +Output Summary: Every `.cs` file in this plan's footprint satisfies its D11 budget after the final format pass. No +budget was breached. The two files that entered this task with one line of headroom were not touched by the +formatter and remain at 1321 and 1002 against budgets of 1322 and 1003. The R9 ordering constraint held: +`QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` finishes at 467 and never passed through 501, because +[P2-T3] removed the relocated member before [P2-T14] added the constructor argument to it in the new partial. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t11-scope.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t11-scope.md new file mode 100644 index 000000000..3c518e9c3 --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t11-scope.md @@ -0,0 +1,151 @@ +# [P3-T11] Scope boundary of the changed source set + +Timestamp: 2026-09-07T08-15 + +Command: `git add --intent-to-add -- '*.cs' '*.csproj'`; `git diff --name-only -- '*.cs' '*.csproj'`; `git status --porcelain --untracked-files=all -- '*.cs' '*.csproj'`; then per-path `git diff --name-only -- ` and `git status --porcelain --untracked-files=all -- ` over the fifteen paths asserted absent + +EXIT_CODE: 0 + +ExpectedExitCode: 0 + +## Anchored diff, name-listing (`git diff --name-only -- '*.cs' '*.csproj'`) + +``` +QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs +QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs +QuickFiler.Test/QuickFiler.Test.csproj +QuickFiler/Controllers/BreadcrumbBridgeRouter.cs +QuickFiler/Controllers/EfcFormController.cs +QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs +QuickFiler/Controllers/QfcItemController.FolderHandling.cs +QuickFiler/Controllers/QfcItemController.ViewerSetup.cs +QuickFiler/QuickFiler.csproj +UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs +UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs +UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs +UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs +UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs +UtilitiesCS.Test/UtilitiesCS.Test.csproj +UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs +UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs +UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs +UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs +UtilitiesCS/UtilitiesCS.csproj +``` + +ENUMERATED-PATH-COUNT: 20 + +## Porcelain status companion (`git status --porcelain --untracked-files=all -- '*.cs' '*.csproj'`) + +``` + M QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs + M QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs + M UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs + M UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs + M UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs + M UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs + M UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs + M UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs + M UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs +``` + +## Why both are listed side by side + +Neither mechanism alone is correct in both states. The anchored diff cannot see an untracked path, which is why +the `git add --intent-to-add` companion runs first. Porcelain status goes empty once a change is committed, which +is why it shows only the nine files the [P3-T1] formatter touched after the Phase 2 commit at `f50fb727` rather +than the whole footprint. The anchored diff is the authoritative enumeration here; the porcelain output is the +untracked-visibility companion and confirms the nine uncommitted rewrites are all inside the same twenty-path set. +The porcelain set is a strict subset of the anchored set, with no path present in one and absent from the other in +the direction that would indicate leakage. + +## The enumerated set is exactly the twenty Write Set paths + +### Nine production paths + +1. `UtilitiesCS/OutlookObjects/Folder/ArchiveStemProjection.cs` — CREATE +2. `UtilitiesCS/OutlookObjects/Folder/ArchiveChainProjection.cs` — CREATE +3. `UtilitiesCS/OutlookObjects/Folder/OutlookFolderHierarchyProvider.cs` — MODIFY +4. `UtilitiesCS/OutlookObjects/Folder/FolderPredictor.cs` — MODIFY +5. `QuickFiler/Controllers/QfcItemController.FolderHandling.cs` — MODIFY +6. `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` — MODIFY +7. `QuickFiler/Controllers/EfcFormController.cs` — MODIFY +8. `QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs` — CREATE +9. `QuickFiler/Controllers/QfcItemController.ViewerSetup.cs` — MODIFY + +### Five new test paths + +10. `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveStemProjectionTests.cs` +11. `UtilitiesCS.Test/OutlookObjects/Folder/ArchiveChainProjectionTests.cs` +12. `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTrimTests.cs` +13. `UtilitiesCS.Test/OutlookObjects/Folder/FolderPredictorRecentsProjectionTests.cs` +14. `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs` + +### Two retargeted test paths + +15. `UtilitiesCS.Test/OutlookObjects/Folder/OutlookFolderHierarchyProviderTests.cs` +16. `QuickFiler.Test/Controllers/QfcItemController.FolderHandlingTests.Part2.cs` + +### Four project files + +17. `UtilitiesCS/UtilitiesCS.csproj` +18. `QuickFiler/QuickFiler.csproj` +19. `UtilitiesCS.Test/UtilitiesCS.Test.csproj` +20. `QuickFiler.Test/QuickFiler.Test.csproj` + +Twenty enumerated, twenty accounted for, none left over in either direction. + +## Per-path no-hunk assertion over the fifteen paths this task names as absent + +Each path was queried individually rather than inferred from absence in the list above, so the assertion is +mechanical. `TRACKED` confirms the path exists in the index at the base commit, which is what makes a zero diff a +real observation rather than the trivially empty result of naming a nonexistent path. + +| Path | TRACKED | anchored diff lines | porcelain lines | +|---|---|---|---| +| UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs | True | 0 | 0 | +| UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.SearchPresentation.cs | True | 0 | 0 | +| UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.cs | True | 0 | 0 | +| UtilitiesCS/OutlookObjects/Folder/BreadcrumbSelectionSession.Highlight.cs | True | 0 | 0 | +| QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs | True | 0 | 0 | +| QuickFiler/Viewers/BreadcrumbBridgeCoordinator.Search.cs | True | 0 | 0 | +| QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | True | 0 | 0 | +| QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs | True | 0 | 0 | +| UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs | True | 0 | 0 | +| UtilitiesCS/OutlookObjects/Folder/ArchiveStemContract.cs | True | 0 | 0 | +| UtilitiesCS/OutlookObjects/Folder/FolderTreeSnapshotQueries.cs | True | 0 | 0 | +| UtilitiesCS/OutlookObjects/Folder/IFolderHierarchyProvider.cs | True | 0 | 0 | +| UtilitiesCS/OutlookObjects/Folder/FolderMinimalWrapper.cs | True | 0 | 0 | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs | True | 0 | 0 | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.Activation.cs | True | 0 | 0 | + +SIBLING-OWNED-FILES-WITH-A-HUNK: 0 of 6 +D8-NO-HUNK-TEST-FILES-WITH-A-HUNK: 0 of 2 +OTHER-EXCLUDED-FILES-WITH-A-HUNK: 0 of 7 + +The first six rows are the sibling-owned files D1 names, which a concurrent sibling item owns and which decision +D-B forbids this item from editing. The last two rows are the two #439 Efc router test files D8 marks NO HUNK: the +AC1/AC2 trim lives inside the provider's GetAncestorChainAsync, below the strict-mock boundary every test in both +files uses, so neither file could need an edit, and editing their shared `Chain` helper would have broken the +unrelated #614 boundary test Issue439SlashOnlyArchiveRootPreservesFullHierarchySelection. All ten tests of that +partial class pass in [P2-T16] and in the [P3-T5] full run, which is the behavioural confirmation that the +no-hunk finding was correct rather than an omission. + +## Scope of this enumeration (R7) + +The pathspec is `'*.cs' '*.csproj'` only. This plan additionally writes evidence artifacts under +`/evidence/` and checks off AC boxes in `spec.md`, and it updates `issue.md` and the plan checklist; none +of those is a source file and none is in this enumeration. That is the intended scope, not an omission. Because +the QuickFiler resources directory contains no `.cs` and no `.csproj` file, an assertion that it is absent from +THIS enumeration would be true for every possible execution and would gate nothing, which is why [P3-T20] makes +the AC8 asset check against its own separately anchored diff rather than reading it off this artifact. + +Output Summary: The changed source set under the R7 pathspec is exactly the twenty Write Set paths — nine +production, five new test, two retargeted test and four project files — with nothing extra and nothing missing. +All fifteen paths this task names as absent were queried individually and every one returned zero anchored-diff +lines and zero porcelain lines while being confirmed tracked at the base commit. The six sibling-owned files carry +no hunk, and the two issue-439 test files carry no hunk. + +## Path hygiene (R3) + +No absolute host path, host account name, or machine name appears in this artifact. diff --git a/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t12-ac8-verification.md b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t12-ac8-verification.md new file mode 100644 index 000000000..8e52be39c --- /dev/null +++ b/docs/features/active/2026-09-06-breadcrumb-lineage-below-archive-root-and-suggestion-path-consistency-799/evidence/qa-gates/p3-t12-ac8-verification.md @@ -0,0 +1,138 @@ +# [P3-T12] AC8 verification — the renderer does not alter a leading underscore + +Timestamp: 2026-09-07T08-20 + +Command: git grep -n -E 'Replace\(["'']_|letter-spacing|text-transform|first-letter|word-break|word-spacing' -- "UtilitiesCS/*.cs" "UtilitiesCS/*.html" "UtilitiesCS/*.css" "QuickFiler/*.cs" "QuickFiler/*.html" "QuickFiler/*.css" "ToDoModel/*.cs" "TaskMaster/*.cs" "Tags/*.cs" "TaskVisualization/*.cs" + +EXIT_CODE: 1 + +ExpectedExitCode: 1 + +`git grep` exits 1 when it matches nothing, and zero matches is this task's SUCCESS outcome. The expectation is +declared explicitly so a passing gate is not normalised to `fail` by an evidence collector that defaults the +expectation to 0. + +## Search result + +OUTPUT-LINES: 0 + +The command produced no output lines at all. The regex, as passed to `git grep -n -E`, was: + +``` +Replace\(["']_|letter-spacing|text-transform|first-letter|word-break|word-spacing +``` + +The pattern was constructed from character codes inside the PowerShell block rather than typed as a literal, +because a bash-hosted single-quoted `pwsh -Command` payload cannot carry an embedded single quote. The pattern +actually passed to git is printed above verbatim from the run, so what was searched is recorded rather than +assumed. + +## The five traced transformations, each re-verified against the current tree + +Each claim below was checked directly in this pass; none is transcribed from the specification without +confirmation. + +### 1. The verbatim splitter inserts nothing and trims nothing + +UtilitiesCS/OutlookObjects/Folder/BreadcrumbRenderProjection.cs, `SplitVerbatim` at lines 242-246, with the +`Split` call itself at line 244: + +``` +private static string[] SplitVerbatim(string verbatimText) +{ + var parts = verbatimText.Split(PathSeparators, StringSplitOptions.RemoveEmptyEntries); + return parts.Length == 0 ? new[] { verbatimText } : parts; +} +``` + +It splits on the path separators and removes empty entries. `String.Split` copies the characters between +separators unchanged; `RemoveEmptyEntries` only discards zero-length parts. A leading underscore is not a +separator and is not zero-length, so it survives verbatim, and no whitespace is introduced anywhere. The +zero-parts fallback returns the original string by reference, so it cannot transform either. + +### 2. The JSON serializer escapes only the double quote, the backslash and control characters + +UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs, `Formatting = Formatting.None` at line 41 and +`JsonConvert.SerializeObject(message, OutboundSettings)` at line 58. `Formatting.None` emits no indentation and, +more to the point, adds no whitespace INSIDE a string value under any formatting setting. Json.NET's string writer +escapes the double quote, the backslash and the C0 control characters; the underscore is none of those and is +written through unchanged. + +### 3. The QuickFiler page assigns segment text through `textContent` + +QuickFiler/Resources/FolderBreadcrumb.html, seven assignments, all through the DOM `textContent` property: + +``` +L253: element.textContent = cell.text; +L262: element.textContent = ""; +L266: element.textContent = cell.kind === "plus" ? "+" : "-"; +L299: selectedPath.textContent = state.selectedFolder; +L309: pct.textContent = row.percentText; +L327: name.textContent = subfolder.displayName; +L357: list.textContent = ""; +``` + +Line 253 is the segment-text assignment. `textContent` sets the node's character data literally: it performs no +HTML parsing, no entity decoding and no escaping, so it cannot introduce a space after an underscore. The page +contains no `innerHTML` assignment for segment text. + +### 4. The Efc page encodes ampersand, less-than, greater-than and quote characters only + +UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs routes every user-visible string through +`WebUtility.HtmlEncode` — at lines 99, 124, 134, 186, 188, 209, 224 and 226. The segment display name is line 188 +and the full path line 186. `WebUtility.HtmlEncode` replaces the markup-significant characters and characters +above the ASCII range with numeric or named references; the underscore (U+005F) and the space (U+0020) are neither +markup-significant nor above the ASCII range, so both pass through byte-for-byte. No non-breaking space is emitted: +a case-insensitive search of the file for `nbsp` returned nothing. + +### 5. Neither stylesheet contains a spacing or casing transform + +There are exactly two breadcrumb stylesheets, both embedded rather than standalone `.css` files: the `