diff --git a/.claude/agent-memory/feature-review/MEMORY.md b/.claude/agent-memory/feature-review/MEMORY.md index c734a8019..553b6a58d 100644 --- a/.claude/agent-memory/feature-review/MEMORY.md +++ b/.claude/agent-memory/feature-review/MEMORY.md @@ -111,6 +111,7 @@ ## Closed-issue residuals (lookup only; details in the residuals index) - [review-residuals index](project_review-residuals-index.md) — per-issue summaries #442-#968 (#930 host-path PARTIAL; #942 `.csproj` disarms C# hook check) +- [1002](project_1002-review-residuals.md) — aria-selected with no CSS = blocking; ratified exemption left AC23 false (cycle 1); cycle 2 PASS; span wrap needs closest() click routing - [985](project_985-review-residuals.md) — cycle 1 blocker: account name in encoded scratchpad dir; cycle 2 PASS; hook sim param is `-RawPayload` - [973](project_973-review-residuals.md) — AC premise defect = autonomous spec amendment; bundled PoshQC FAIL row + vacuous PASS - [438](project_438_cycle1_findings.md) · [440](project_440-review-residuals.md) · [441](project_441-review-residuals-and-494-handoff.md) · [457](project_457-review-residuals.md) · [464](project_464-review-residuals.md) · [489](project_489-review-residuals.md) · [493](project_493-review-residuals-and-msbuild-log-gate-adjudication.md) · [511](project_511-rescope-review-residuals.md) · [553](project_553_ci_split_review_pattern.md) · [565](project_565-review-residuals.md) · [584](project_584-review-residuals.md) diff --git a/.claude/agent-memory/feature-review/project_1002-review-residuals.md b/.claude/agent-memory/feature-review/project_1002-review-residuals.md new file mode 100644 index 000000000..c19607a1f --- /dev/null +++ b/.claude/agent-memory/feature-review/project_1002-review-residuals.md @@ -0,0 +1,27 @@ +--- +name: 1002-review-residuals +description: "#1002 (EFC descendant tree, trash preselect, QFC nesting): cycle 0 4 blocking; cycle 1 1 autonomous (AC23 vs ratified exemption); cycle 2 14-10 PASS 0 blocking" +metadata: + type: project +--- + +Full-bug review, base 8e4ad1ecb, session cwd = review tree (no mirror). All artifacts validated first try with `validate_orchestration_artifacts` using the #985 policy-audit shape (Template Provenance, 7-col metrics table incl. a JavaScript row, checklist bullets, 1.2.1 bullets, 1.2.2 verdict paragraphs). + +**Cycle 0 (head e96e65302, 2026-10-22T10-39):** 26/26 AC PASS, 4 blocking (3 autonomous, 1 human). +- A renderer that marks selection by attribute (`aria-selected="true"`) needs a matching CSS rule; grep the asset CSS for the attribute. None existed -> blocking autonomous (B-4). +- Modified-file floor split: coverage LOWERED by the diff = autonomous; pre-existing MessageBox/WebView2 residue = human_decision_required (CLAUDE.md UT2 exemption needs maintainer ratification); trivial `?? throw` guards at 50% branch = autonomous. +- Hook simulation: `Invoke-FeatureReviewCoverageValidation -InputText (@{last_assistant_message=$t; stop_hook_active=$false}|ConvertTo-Json)`. +- Evidence filename stamps are UTC; local clock is -0400 — use `date -u` for the review stamp. + +**Cycle 1 re-audit (head b72ac1dbb, 2026-10-22T13-37):** B-1..B-4 resolved; 1 new blocking R-1 autonomous. +- Reusable: when a maintainer ratifies an exemption mid-loop, grep the spec AC text for clauses it falsifies ("no coverage exclusion is added"). The decision lifted the plan's Do-Not-Do but nobody amended AC23, which stayed checked -> AC FAIL, autonomous clerical amendment (reclass route to human stated). See [[973-review-residuals]] for the AC-amendment precedent. +- Member-level `[ExcludeFromCodeCoverage]` on async methods removes the state machine and lambdas from Cobertura too (no residual class nodes); valid-line counts drop (Actions 111->28). +- net48 `ExcludeFromCodeCoverageAttribute` has no `Justification` (CS0246); comment form is correct. +- Freshness check that worked: trx `` + per-test `outcome=` grep in `coverage/test-results/mstest-coverage-run.trx`, and source mtimes earlier than the trx. +- Reused scratchpad `filecov.py` (args: repo xml base files...) for per-file + changed-line union. + +**Cycle 2 re-audit (head 54b36112a, 2026-10-22T14-10):** PASS, 0 blocking; 26/26 AC PASS. R-1 resolved by a clause-only AC23 edit plus `## Planner Amendments`. All three artifacts validated on the first try in the same shape. +- Wrapping JS child text in a span is safe only if every click branch resolves its target with `closest(...)`; grep the handler, not just the test. +- Orchestrator AC9 ruling: a client-side transient render (bridge `subfolderResult`) is not governed by a "server render unchanged" AC. + +**Follow-ups owed:** manual Outlook check pending (AC26, non-blocking). diff --git a/.claude/agent-memory/task-researcher/index_quickfiler_efc_defects.md b/.claude/agent-memory/task-researcher/index_quickfiler_efc_defects.md index 5a2cae72e..dffcbb176 100644 --- a/.claude/agent-memory/task-researcher/index_quickfiler_efc_defects.md +++ b/.claude/agent-memory/task-researcher/index_quickfiler_efc_defects.md @@ -7,6 +7,8 @@ metadata: Sub-index split out of MEMORY.md (2026-10-09) to keep the root index under its read limit. One line per memory file. +- [efc-qfc-separate-breadcrumb-surfaces-1002](project_efc_qfc_separate_breadcrumb_surfaces_1002.md) — EFC and QFC pickers are separate surfaces; bind clears selection; .mjs not in CI + - [sortemail-split-prompt-seam-956](project_sortemail_split_and_prompt_seam_956.md) — per-call prompt session; AsyncLocal breaks stickiness - [sortemail-966-consolidation-959](project_sortemail_966_consolidation_959.md) — live path builds helpers with FOLDER NAME; ToDoModel copy uncompiled - [sortemail-latent-defects-959](project_sortemail_latent_defects_959.md) — callers discard the bool; L4 has a null-array defect diff --git a/.claude/agent-memory/task-researcher/project_efc_qfc_separate_breadcrumb_surfaces_1002.md b/.claude/agent-memory/task-researcher/project_efc_qfc_separate_breadcrumb_surfaces_1002.md new file mode 100644 index 000000000..3dda022ec --- /dev/null +++ b/.claude/agent-memory/task-researcher/project_efc_qfc_separate_breadcrumb_surfaces_1002.md @@ -0,0 +1,12 @@ +--- +name: efc-qfc-separate-breadcrumb-surfaces-1002 +description: EFC and QFC folder pickers are two unrelated WebView2 surfaces; #997 descendant code never reaches EFC; node .mjs suites are not in TaskMaster CI +metadata: + type: project +--- + +EFC folder area = `QuickFiler.Controllers.BreadcrumbBridgeRouter` + UtilitiesCS `BreadcrumbRow`/`BreadcrumbHtmlRenderer`/`BreadcrumbDocumentAssets.BridgeJs` (C# string JS, NavigateToString). QFC = `FolderBreadcrumb.html/.js` + `FolderBreadcrumbBridgeRouter` + `BreadcrumbSelectionSession` + `BreadcrumbDescendantNode` (internal; UtilitiesCS grants no InternalsVisibleTo QuickFiler). They share only `BreadcrumbRowBuilder.BannerPrefix`. + +**Why:** #1002 research (2026-10-22) found a "port the QFC fix to EFC" request means building EFC's own keyed tree; also EFC `BindRowsAsync` clears selection then re-navigates, so a selection posted as a `render` message right after a bind can race the navigation. + +**How to apply:** for any EFC/QFC "same fix" request, check which surface first; preselect inside the bind (rendered document) rather than post-bind messages; `tests/quickfiler-breadcrumb/*.mjs` give no CI signal, so pair them with C# text-contract tests. See [[percoverage-epic-136]] for QuickFiler coverage context. diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs new file mode 100644 index 000000000..3d34e1212 --- /dev/null +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs @@ -0,0 +1,487 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +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 +{ + /// + /// Issue 1002 EFC descendant routing through the real router: identity-addressed toggle and + /// activation over strict provider and host mocks, with node identities read from the + /// data-node-id attributes of the captured render posts. + /// + [TestClass] + public class BreadcrumbBridgeRouterIssue1002Tests + { + private EfcDescendantRouterFixture fixture; + + [TestInitialize] + public void Setup() + { + fixture = new EfcDescendantRouterFixture(); + } + + [TestMethod] + public async Task DescendantToggle_SecondLevelNode_FetchesWithNodeKeyAndRendersChildren() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + int before = fixture.Posts.Count; + + // Act + await fixture.ToggleAsync("row-0", q3); + + // Assert + fixture.Provider.Verify( + p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(EfcDescendantRouterFixture.Q3), + It.IsAny() + ), + Times.Once + ); + fixture.Posts.Count.Should().Be(before + 1, "one row render carries the children"); + string html = fixture.LatestRender(); + html.Should() + .Contain("data-depth=\"1\" title=\"" + EfcDescendantRouterFixture.Invoices); + html.IndexOf(">Q3", StringComparison.Ordinal) + .Should() + .BeLessThan(html.IndexOf(">Invoices", StringComparison.Ordinal)); + } + + [TestMethod] + public async Task DescendantToggle_ExpandedNode_CollapsesWithoutProviderCall() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + await fixture.ToggleAsync("row-0", q3); + int before = fixture.Posts.Count; + + // Act + await fixture.ToggleAsync("row-0", q3); + + // Assert + fixture.Provider.Verify( + p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(EfcDescendantRouterFixture.Q3), + It.IsAny() + ), + Times.Once + ); + fixture.Posts.Count.Should().Be(before + 1, "the collapse posts one row render"); + fixture.LatestRender().Should().NotContain("Invoices"); + } + + [TestMethod] + public async Task DescendantToggle_DoesNotChangeSelection() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + await fixture.InboundAsync("rowSelected", "row-0"); + string selection = fixture.Router.SelectedFolderPath; + fixture.SelectionEvents.Clear(); + + // Act + await fixture.ToggleAsync("row-0", q3); + await fixture.ToggleAsync("row-0", q3); + + // Assert + fixture.Router.SelectedFolderPath.Should().Be(selection); + fixture.SelectionEvents.Should().BeEmpty("a toggle changes expansion only"); + } + + [TestMethod] + public async Task DescendantToggle_PostsNoSubfolderResult() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + int before = fixture.Posts.Count; + + // Act + await fixture.ToggleAsync("row-0", q3); + + // Assert + fixture + .Posts.Skip(before) + .Should() + .NotContain(post => post.Contains("subfolderResult")); + } + + [TestMethod] + public async Task DescendantActivate_Grandchild_CommitsArchiveRelativeStem() + { + // Arrange + string invoices = await fixture.ExpandToInvoicesAsync(); + fixture.SelectionEvents.Clear(); + + // Act + await fixture.ActivateAsync("row-0", invoices); + + // Assert + const string stem = @"Clients\North\Q3\Invoices"; + fixture.Router.SelectedFolderPath.Should().Be(stem); + fixture.SelectionEvents.Should().Equal(new[] { stem }, "one change event is raised"); + EfcSelectionGuard.IsValidFilingSelection(stem).Should().BeTrue(); + string html = fixture.LatestRender(); + html.Should().Contain("class=\"rowwrap selected\" data-row-id=\"row-0\""); + html.Should() + .Contain( + "data-node-id=\"" + + invoices + + "\" data-depth=\"1\" title=\"" + + EfcDescendantRouterFixture.Invoices + + "\" aria-selected=\"true\"" + ); + } + + [TestMethod] + public async Task DescendantActivate_ThenRowConfirmed_RaisesConfirmationOnce() + { + // Arrange + string invoices = await fixture.ExpandToInvoicesAsync(); + await fixture.ActivateAsync("row-0", invoices); + + // Act + await fixture.InboundAsync("rowConfirmed", "row-0"); + + // Assert + fixture.Confirmations.Should().Be(1); + fixture.Router.SelectedFolderPath.Should().Be(@"Clients\North\Q3\Invoices"); + } + + [TestMethod] + public async Task DescendantActivate_OutsideArchiveRoot_LeavesSelectionAndHighlightUnchanged() + { + // Arrange + fixture.Children[EfcDescendantRouterFixture.Q3] = new[] + { + EfcDescendantRouterFixture.Segment( + EfcDescendantRouterFixture.Invoices, + "Invoices", + false + ), + EfcDescendantRouterFixture.Segment( + EfcDescendantRouterFixture.Receipts, + "Receipts", + true + ), + EfcDescendantRouterFixture.Segment( + @"\\other\Mailbox\Elsewhere", + "Elsewhere", + false + ), + }; + string invoices = await fixture.ExpandToInvoicesAsync(); + await fixture.ActivateAsync("row-0", invoices); + string elsewhere = fixture.NodeId(@"\\other\Mailbox\Elsewhere"); + string receipts = fixture.NodeId(EfcDescendantRouterFixture.Receipts); + fixture.SelectionEvents.Clear(); + int before = fixture.Posts.Count; + + // Act + await fixture.ActivateAsync("row-0", elsewhere); + + // Assert + fixture.Router.SelectedFolderPath.Should().Be(@"Clients\North\Q3\Invoices"); + fixture.SelectionEvents.Should().BeEmpty(); + fixture.Posts.Count.Should().Be(before, "a non-selection posts nothing"); + await fixture.ToggleAsync("row-0", receipts); + string html = fixture.LatestRender(); + html.Should().Contain("data-node-id=\"" + invoices + "\""); + Regex.Matches(html, "aria-selected=\"true\"").Count.Should().Be(1); + html.Should() + .Contain( + WebUtility.HtmlEncode(EfcDescendantRouterFixture.Invoices) + + "\" aria-selected=\"true\"", + "the prior descendant keeps the highlight" + ); + } + + [TestMethod] + public async Task DescendantRequest_UnknownNode_IsRejectedWithoutStateChange() + { + // Arrange + await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + await fixture.InboundAsync("rowSelected", "row-0"); + string selection = fixture.Router.SelectedFolderPath; + fixture.SelectionEvents.Clear(); + int before = fixture.Posts.Count; + const string unknown = "ffffffffffffffffffffffffffffffff"; + + // Act + await fixture.ToggleAsync("row-0", unknown); + await fixture.ActivateAsync("row-0", unknown); + await fixture.ToggleAsync("row-9", unknown); + await fixture.ActivateAsync("row-9", unknown); + + // Assert + fixture.Posts.Count.Should().Be(before, "a rejected identity posts nothing"); + fixture.SelectionEvents.Should().BeEmpty(); + fixture.Router.SelectedFolderPath.Should().Be(selection); + fixture.Provider.Verify( + p => + p.GetImmediateSubfoldersAsync( + It.IsAny(), + It.IsAny() + ), + Times.Once, + "only the first-level expansion reached the provider" + ); + } + + [TestMethod] + public async Task DescendantToggle_LeafNode_IsRejectedWithoutProviderCall() + { + // Arrange + string q4 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q4); + int before = fixture.Posts.Count; + + // Act + await fixture.ToggleAsync("row-0", q4); + + // Assert + fixture.Posts.Count.Should().Be(before); + fixture.Provider.Verify( + p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(EfcDescendantRouterFixture.Q4), + It.IsAny() + ), + Times.Never + ); + } + + [TestMethod] + public async Task DescendantRequest_NodeFromAnotherRow_IsRejected() + { + // Arrange + await fixture.BindAsync(EfcDescendantRouterFixture.Presented, @"Clients\South"); + await fixture.InboundAsync("leafExpandToggle", "row-0"); + string q3 = fixture.NodeId(EfcDescendantRouterFixture.Q3); + await fixture.InboundAsync("leafExpandToggle", "row-1"); + fixture.SelectionEvents.Clear(); + int before = fixture.Posts.Count; + + // Act + await fixture.ToggleAsync("row-1", q3); + await fixture.ActivateAsync("row-1", q3); + + // Assert + fixture.Posts.Count.Should().Be(before, "row-1 never issued that identity"); + fixture.SelectionEvents.Should().BeEmpty(); + fixture.Router.SelectedFolderPath.Should().BeNull(); + fixture.Provider.Verify( + p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(EfcDescendantRouterFixture.Q3), + It.IsAny() + ), + Times.Never + ); + } + + [TestMethod] + public async Task ExpandLeaf_KeyedChildren_RenderNodeAffordances() + { + // Arrange + await fixture.BindAsync(EfcDescendantRouterFixture.Presented); + + // Act + await fixture.InboundAsync("leafExpandToggle", "row-0"); + + // Assert + string html = fixture.LatestRender(); + Regex.Matches(html, "data-role=\"node\"").Count.Should().Be(1, "only Q3 has children"); + html.IndexOf(">Q3", StringComparison.Ordinal) + .Should() + .BeGreaterThan(-1) + .And.BeLessThan(html.IndexOf("data-role=\"node\"", StringComparison.Ordinal)); + } + } + + /// + /// Shared in-memory fixture for the issue 1002 EFC router tests: a real router over strict + /// provider and host mocks, with an archive root bound and a three-level folder tree. + /// + internal sealed class EfcDescendantRouterFixture + { + internal const string Root = @"\\mailbox\Archive"; + internal const string Presented = @"Clients\North"; + internal const string North = Root + @"\Clients\North"; + internal const string Q3 = North + @"\Q3"; + internal const string Q4 = North + @"\Q4"; + internal const string Invoices = Q3 + @"\Invoices"; + internal const string Receipts = Q3 + @"\Receipts"; + + internal EfcDescendantRouterFixture() + { + Children = new Dictionary + { + [North] = new[] { Segment(Q3, "Q3", true), Segment(Q4, "Q4", false) }, + [Q3] = new[] + { + Segment(Invoices, "Invoices", false), + Segment(Receipts, "Receipts", true), + }, + [Root + @"\Clients\South"] = new[] + { + Segment(Root + @"\Clients\South\S1", "S1", true), + }, + }; + Host.SetupGet(h => h.IsCoreInitialized).Returns(() => CoreInitialized); + Host.Setup(h => h.NavigateToString(It.IsAny())).Callback(Documents.Add); + Host.Setup(h => h.PostMessageJson(It.IsAny())).Callback(Posts.Add); + Provider + .Setup(p => + p.ResolveLeafKeyAsync(It.IsAny(), It.IsAny()) + ) + .ReturnsAsync((string path, CancellationToken token) => Key(path)); + Provider + .Setup(p => + p.GetAncestorChainAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync((FolderTreeNodeKey key, CancellationToken token) => Chain(key)); + Provider + .Setup(p => + p.GetImmediateSubfoldersAsync( + It.IsAny(), + It.IsAny() + ) + ) + .ReturnsAsync( + (FolderTreeNodeKey key, CancellationToken token) => ChildrenOf(key.FolderPath) + ); + Router = new BreadcrumbBridgeRouter( + Provider.Object, + Host.Object, + new BreadcrumbMessageCodec(), + new BreadcrumbHtmlRenderer(), + new BreadcrumbOutboundQueue(Host.Object) + ); + Router.SelectedFolderPathChanged += (sender, path) => SelectionEvents.Add(path); + Router.ConfirmationRequested += (sender, args) => Confirmations++; + } + + internal Mock Provider { get; } = + new Mock(MockBehavior.Strict); + + internal Mock Host { get; } = + new Mock(MockBehavior.Strict); + + internal BreadcrumbBridgeRouter Router { get; } + + internal Dictionary Children { get; } + + internal List Posts { get; } = new List(); + + internal List Documents { get; } = new List(); + + internal List SelectionEvents { get; } = new List(); + + internal int Confirmations { get; private set; } + + internal bool CoreInitialized { get; set; } = true; + + internal Task BindAsync(params string[] rows) => + Router.BindRowsAsync(rows, Array.Empty(), Root, CancellationToken.None); + + internal Task InboundAsync(string type, string rowId) => + Router.ProcessInboundAsync("{\"type\":\"" + type + "\",\"rowId\":\"" + rowId + "\"}"); + + internal Task ToggleAsync(string rowId, string nodeId) => + NodeMessageAsync("descendantToggle", rowId, nodeId); + + internal Task ActivateAsync(string rowId, string nodeId) => + NodeMessageAsync("descendantActivate", rowId, nodeId); + + /// Binds the presented row, expands its leaf, and returns the node id of a child. + internal async Task BindAndExpandAsync(string childPath) + { + await BindAsync(Presented); + await InboundAsync("leafExpandToggle", "row-0"); + return NodeId(childPath); + } + + /// Expands the leaf and Q3, then returns the node id of the Invoices grandchild. + internal async Task ExpandToInvoicesAsync() + { + string q3 = await BindAndExpandAsync(Q3); + await ToggleAsync("row-0", q3); + return NodeId(Invoices); + } + + /// The HTML of the most recent posted render message, JSON-unescaped. + internal string LatestRender() + { + string post = Posts.LastOrDefault(json => json.Contains("\"type\":\"render\"")); + post.Should().NotBeNull("a render message must have been posted"); + return Regex.Unescape(post); + } + + /// Reads the node identity rendered for a folder path from the latest render. + internal string NodeId(string folderPath) + { + System.Text.RegularExpressions.Match match = Regex.Match( + LatestRender(), + "data-node-id=\"([0-9a-f]+)\" data-depth=\"\\d+\" title=\"" + + Regex.Escape(WebUtility.HtmlEncode(folderPath)) + + "\"" + ); + match.Success.Should().BeTrue($"the latest render must carry a node for {folderPath}"); + return match.Groups[1].Value; + } + + internal static FolderTreeNodeKey Key(string path) => + new FolderTreeNodeKey("store", path, path); + + internal static FolderBreadcrumbSegment Segment( + string path, + string name, + bool hasChildren + ) => new FolderBreadcrumbSegment(Key(path), name, path, hasChildren); + + private Task NodeMessageAsync(string type, string rowId, string nodeId) => + Router.ProcessInboundAsync( + "{\"type\":\"" + + type + + "\",\"rowId\":\"" + + rowId + + "\",\"nodeId\":\"" + + nodeId + + "\"}" + ); + + private IReadOnlyList ChildrenOf(string path) + { + FolderBreadcrumbSegment[] children; + return Children.TryGetValue(path, out children) + ? children + : Array.Empty(); + } + + // The ancestor chain of Root\Clients\: the Clients folder, then the leaf itself. + private static IReadOnlyList Chain(FolderTreeNodeKey key) + { + string leaf = key.FolderPath.Substring(key.FolderPath.LastIndexOf('\\') + 1); + return new[] + { + Segment(Root + @"\Clients", "Clients", true), + Segment(key.FolderPath, leaf, true), + }; + } + } +} diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs new file mode 100644 index 000000000..e3e7e7aeb --- /dev/null +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs @@ -0,0 +1,259 @@ +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 QuickFiler.Test.Controllers +{ + /// + /// Issue 1002 stale-completion guards: a provider task held open on a + /// barrier completes after a collapse, a rebind, or a + /// later toggle, and the router must attach and post nothing for the superseded request. The + /// same guard covers the existing first-level leaf expansion. + /// + [TestClass] + public class BreadcrumbBridgeRouterStaleCompletionIssue1002Tests + { + private EfcDescendantRouterFixture fixture; + + [TestInitialize] + public void Setup() + { + fixture = new EfcDescendantRouterFixture(); + } + + [TestMethod] + public async Task DescendantFetch_CompletesAfterCollapse_AttachesNothing() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + var barrier = HoldFetch(EfcDescendantRouterFixture.Q3); + Task pending = fixture.ToggleAsync("row-0", q3); + await fixture.ToggleAsync("row-0", q3); + int before = fixture.Posts.Count; + + // Act + barrier.SetResult(SecondLevel()); + await pending; + + // Assert + fixture.Posts.Count.Should().Be(before, "the collapsed node rejects the late result"); + fixture.LatestRender().Should().NotContain("Invoices"); + } + + [TestMethod] + public async Task DescendantFetch_CompletesAfterRebind_PostsNothingForReplacedRow() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + var barrier = HoldFetch(EfcDescendantRouterFixture.Q3); + Task pending = fixture.ToggleAsync("row-0", q3); + await fixture.BindAsync(EfcDescendantRouterFixture.Presented); + int before = fixture.Posts.Count; + + // Act + barrier.SetResult(SecondLevel()); + await pending; + + // Assert + fixture.Posts.Count.Should().Be(before, "row-0 now names a different row instance"); + } + + [TestMethod] + public async Task DescendantFetch_ReverseCompletion_OnlyLatestGenerationApplies() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + var first = new TaskCompletionSource>(); + var second = new TaskCompletionSource>(); + fixture + .Provider.SetupSequence(p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(EfcDescendantRouterFixture.Q3), + It.IsAny() + ) + ) + .Returns(first.Task) + .Returns(second.Task); + Task firstToggle = fixture.ToggleAsync("row-0", q3); + await fixture.ToggleAsync("row-0", q3); + Task secondToggle = fixture.ToggleAsync("row-0", q3); + + // Act + second.SetResult(SecondLevel()); + await secondToggle; + int afterLatest = fixture.Posts.Count; + first.SetResult( + new[] + { + EfcDescendantRouterFixture.Segment( + EfcDescendantRouterFixture.Q3 + @"\Stale", + "Stale", + false + ), + } + ); + await firstToggle; + + // Assert + fixture.Posts.Count.Should().Be(afterLatest, "the superseded result posts nothing"); + string html = fixture.LatestRender(); + html.Should().Contain(">Invoices"); + html.Should().NotContain("Stale"); + } + + [TestMethod] + public async Task DescendantFetch_ProviderFault_LeavesStateUnchangedAndAllowsRetry() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + fixture + .Provider.SetupSequence(p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(EfcDescendantRouterFixture.Q3), + It.IsAny() + ) + ) + .ThrowsAsync(new InvalidOperationException("provider fault")) + .ReturnsAsync(SecondLevel()); + int before = fixture.Posts.Count; + + // Act + Func faulted = () => fixture.ToggleAsync("row-0", q3); + + // Assert + await faulted.Should().NotThrowAsync("the provider fault is logged at the boundary"); + fixture.Posts.Count.Should().Be(before, "a faulted fetch posts nothing"); + await fixture.ToggleAsync("row-0", q3); + fixture.Posts.Count.Should().Be(before + 1, "the retry renders the children"); + fixture.LatestRender().Should().Contain(">Invoices"); + } + + [TestMethod] + public async Task DescendantFetch_Canceled_LeavesStateUnchanged() + { + // Arrange + string q3 = await fixture.BindAndExpandAsync(EfcDescendantRouterFixture.Q3); + fixture + .Provider.SetupSequence(p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(EfcDescendantRouterFixture.Q3), + It.IsAny() + ) + ) + .ThrowsAsync(new OperationCanceledException()) + .ReturnsAsync(SecondLevel()); + string renderBefore = fixture.LatestRender(); + int before = fixture.Posts.Count; + + // Act + await fixture.ToggleAsync("row-0", q3); + + // Assert + fixture.Posts.Count.Should().Be(before, "a canceled fetch posts nothing"); + fixture.Router.SelectedFolderPath.Should().BeNull(); + await fixture.ToggleAsync("row-0", q3); + fixture.Provider.Verify( + p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(EfcDescendantRouterFixture.Q3), + It.IsAny() + ), + Times.Exactly(2), + "the node was left collapsed, so the next toggle fetches again" + ); + renderBefore.Should().Contain("aria-expanded=\"false\""); + } + + [TestMethod] + public async Task ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow() + { + // Arrange + await fixture.BindAsync(EfcDescendantRouterFixture.Presented); + var barrier = HoldFetch(EfcDescendantRouterFixture.North); + Task pending = fixture.InboundAsync("leafExpandToggle", "row-0"); + await fixture.BindAsync(EfcDescendantRouterFixture.Presented); + int before = fixture.Posts.Count; + + // Act + barrier.SetResult( + new[] + { + EfcDescendantRouterFixture.Segment(EfcDescendantRouterFixture.Q3, "Q3", true), + } + ); + await pending; + + // Assert + fixture.Posts.Count.Should().Be(before, "the replaced row receives no post"); + fixture + .Posts.Skip(before) + .Should() + .NotContain(post => post.Contains("subfolderResult")); + } + + [TestMethod] + public async Task ExpandLeaf_CurrentRow_StillPostsSubfolderResultThenRender() + { + // Arrange + await fixture.BindAsync(EfcDescendantRouterFixture.Presented); + var barrier = HoldFetch(EfcDescendantRouterFixture.North); + Task pending = fixture.InboundAsync("leafExpandToggle", "row-0"); + int before = fixture.Posts.Count; + + // Act + barrier.SetResult( + new[] + { + EfcDescendantRouterFixture.Segment(EfcDescendantRouterFixture.Q3, "Q3", true), + } + ); + await pending; + + // Assert + List posted = fixture.Posts.Skip(before).ToList(); + posted + .Should() + .HaveCount(2, "the current row keeps the subfolderResult-then-render pair"); + posted[0] + .Should() + .Contain("\"type\":\"subfolderResult\"") + .And.Contain("\"rowId\":\"row-0\""); + posted[1].Should().Contain("\"type\":\"render\"").And.Contain("\"rowId\":\"row-0\""); + } + + private TaskCompletionSource> HoldFetch(string path) + { + var barrier = new TaskCompletionSource>(); + fixture + .Provider.Setup(p => + p.GetImmediateSubfoldersAsync( + EfcDescendantRouterFixture.Key(path), + It.IsAny() + ) + ) + .Returns(barrier.Task); + return barrier; + } + + private static FolderBreadcrumbSegment[] SecondLevel() => + new[] + { + EfcDescendantRouterFixture.Segment( + EfcDescendantRouterFixture.Invoices, + "Invoices", + false + ), + EfcDescendantRouterFixture.Segment( + EfcDescendantRouterFixture.Receipts, + "Receipts", + true + ), + }; + } +} diff --git a/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs new file mode 100644 index 000000000..389803f7c --- /dev/null +++ b/QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UtilitiesCS; + +namespace QuickFiler.Test.Controllers +{ + /// + /// Issue 1002 trash preselection: a preferred row passed to the router bind is selected + /// before the document is delivered, so the navigated (or pending) document already marks it + /// and no follow-up render message is needed. + /// + [TestClass] + public class BreadcrumbBridgeRouterTrashIssue1002Tests + { + private const string Trash = "Trash to Delete"; + private const string TrashRowSelected = "class=\"rowwrap selected\" data-row-id=\"row-0\""; + + private EfcDescendantRouterFixture fixture; + + [TestInitialize] + public void Setup() + { + fixture = new EfcDescendantRouterFixture(); + } + + [TestMethod] + public async Task BindRows_PreferredTrashRow_SelectsTrashInDeliveredDocument() + { + // Act + await BindWithPreferredAsync(Trash); + + // Assert + fixture.Router.SelectedFolderPath.Should().Be(Trash); + fixture.Documents.Should().HaveCount(1, "one document is navigated"); + fixture.Documents[0].Should().Contain(TrashRowSelected); + } + + [TestMethod] + public async Task BindRows_PreferredTrashRow_LastSelectionNotificationCarriesTrash() + { + // Arrange: an earlier selection makes the bind clear it first (#499). + await fixture.BindAsync(EfcDescendantRouterFixture.Presented); + await fixture.InboundAsync("rowSelected", "row-0"); + fixture.SelectionEvents.Clear(); + + // Act + await BindWithPreferredAsync(Trash); + + // Assert + fixture.SelectionEvents.Count(path => path == Trash).Should().Be(1); + fixture + .SelectionEvents.Last() + .Should() + .Be(Trash, "the trash selection is the last word"); + } + + [TestMethod] + public async Task BindRows_PreferredTrashRow_PostsNoFollowUpRender() + { + // Arrange + int before = fixture.Posts.Count; + + // Act + await BindWithPreferredAsync(Trash); + + // Assert + fixture.Posts.Count.Should().Be(before, "the selection travels inside the document"); + } + + [TestMethod] + public async Task BindRows_PreferredRowAbsent_SelectsNothing() + { + // Act + await BindWithPreferredAsync(@"Clients\Missing"); + + // Assert + fixture.Router.SelectedFolderPath.Should().BeNull(); + fixture.SelectionEvents.Should().BeEmpty(); + fixture.Documents.Single().Should().NotContain("rowwrap selected"); + } + + [TestMethod] + public async Task BindRows_PreferredRowOutsideArchiveRoot_SelectsNothing() + { + // Arrange: a rooted target outside the bound archive root is a #614 non-selection. + const string outside = @"\\other\Mailbox\Outside"; + + // Act + await fixture.Router.BindRowsAsync( + new[] { outside }, + Array.Empty(), + EfcDescendantRouterFixture.Root, + CancellationToken.None, + outside + ); + + // Assert + fixture.Router.SelectedFolderPath.Should().BeNull("the shared row rule refuses it"); + fixture.SelectionEvents.Should().BeEmpty(); + fixture.Documents.Single().Should().NotContain("rowwrap selected"); + } + + [TestMethod] + public async Task BindRows_NoPreferredRow_KeepsExistingClearBehavior() + { + // Arrange + await fixture.BindAsync(EfcDescendantRouterFixture.Presented); + await fixture.InboundAsync("rowSelected", "row-0"); + fixture.SelectionEvents.Clear(); + + // Act + await fixture.Router.BindRowsAsync( + new[] { Trash, EfcDescendantRouterFixture.Presented }, + Array.Empty(), + EfcDescendantRouterFixture.Root, + CancellationToken.None + ); + + // Assert + fixture.Router.SelectedFolderPath.Should().BeNull("the stale selection is cleared"); + fixture.SelectionEvents.Should().Equal(new string[] { null }); + fixture.Documents.Last().Should().NotContain("rowwrap selected"); + } + + [TestMethod] + public async Task BindRows_PreferredTrashRowBeforeCoreInit_PendingDocumentCarriesSelection() + { + // Arrange + fixture.CoreInitialized = false; + await BindWithPreferredAsync(Trash); + fixture.Documents.Should().BeEmpty("the document waits for core initialization"); + fixture.CoreInitialized = true; + + // Act + fixture.Router.NotifyCoreInitialized(); + + // Assert + fixture.Documents.Should().ContainSingle().Which.Should().Contain(TrashRowSelected); + fixture.Router.SelectedFolderPath.Should().Be(Trash); + } + + private Task BindWithPreferredAsync(string preferredRowText) => + fixture.Router.BindRowsAsync( + new[] { Trash, EfcDescendantRouterFixture.Presented }, + Array.Empty(), + EfcDescendantRouterFixture.Root, + CancellationToken.None, + preferredRowText + ); + } +} diff --git a/QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs b/QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs new file mode 100644 index 000000000..90279a8f2 --- /dev/null +++ b/QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using QuickFiler.Viewers; +using UtilitiesCS; +using UtilitiesCS.OutlookObjects.Folder; + +namespace QuickFiler.Controllers.Tests +{ + /// + /// Issue 1002 delete gesture end to end: ActionDeleteAsync on a controller wired to a + /// real router over strict mocks selects the trash row inside the bound document, and a bind + /// fault is still reported at the bind boundary. + /// + public partial class EfcFormControllerTests + { + private const string Issue1002ArchiveRoot = @"\Archive"; + private const string Issue1002Presented = @"Clients\North"; + private const string Issue1002TrashSelected = + "class=\"rowwrap selected\" data-row-id=\"row-0\""; + + [TestMethod] + public async Task ActionDeleteAsync_WithRouter_SelectsTrashEntry() + { + // Arrange + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: true); + + // Act + await controller.ActionDeleteAsync(); + + // Assert + controller.SelectedFolder.Should().Be(EfcFormController.TrashRowText); + } + + [TestMethod] + public async Task ActionDeleteAsync_WithRouter_DeliveredDocumentMarksTrashSelected() + { + // Arrange + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: true); + + // Act + await controller.ActionDeleteAsync(); + + // Assert + documents.Should().NotBeEmpty("the gesture binds and navigates a document"); + documents.Last().Should().Contain(Issue1002TrashSelected); + } + + [TestMethod] + public async Task ActionDeleteAsync_AwaitedTwice_KeepsTrashSelectedAndSingleTrashRow() + { + // Arrange + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: true); + + // Act + await controller.ActionDeleteAsync(); + await controller.ActionDeleteAsync(); + + // Assert + controller.SelectedFolder.Should().Be(EfcFormController.TrashRowText); + documents.Last().Should().Contain(Issue1002TrashSelected); + var folderRows = (string[])GetPrivateField(controller, "_folderRows"); + folderRows.Count(row => row == EfcFormController.TrashRowText).Should().Be(1); + } + + [TestMethod] + public async Task ActionDeleteAsync_BindFault_IsReportedAtBindBoundary() + { + // Arrange: with _globals unset the bind boundary faults reading the archive root. + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: false); + var reported = new List(); + controller.BoundaryErrorSink = (message, exception) => reported.Add(exception); + + // Act + Func act = () => controller.ActionDeleteAsync(); + + // Assert + await act.Should().NotThrowAsync("the bind boundary contains its own fault"); + reported.Should().ContainSingle("the fault is reported once at the bind boundary"); + documents.Should().BeEmpty("no document is delivered by a faulted bind"); + } + + [TestMethod] + public async Task BindSourceFolderRows_WithViewerAndRouter_RetainsRowsAndBindsDocument() + { + // Arrange + var documents = new List(); + var delivered = new TaskCompletionSource(); + EfcFormController controller = CreateIssue1002Controller( + documents, + withGlobals: true, + delivered: delivered + ); + string[] rows = { Issue1002Presented }; + + // Act + InvokeIssue1002Bind(controller, "BindSourceFolderRows", rows); + await delivered.Task; + + // Assert + GetPrivateField(controller, "_folderRows").Should().BeSameAs(rows); + documents.Should().ContainSingle(); + documents[0].Should().Contain("North", "the delivered document carries the row"); + } + + [TestMethod] + public async Task BindSourceFolderRows_NullRows_RetainsEmptyArray() + { + // Arrange + var documents = new List(); + var delivered = new TaskCompletionSource(); + EfcFormController controller = CreateIssue1002Controller( + documents, + withGlobals: true, + delivered: delivered + ); + + // Act + InvokeIssue1002Bind(controller, "BindSourceFolderRows", null); + await delivered.Task; + + // Assert + ((string[])GetPrivateField(controller, "_folderRows")) + .Should() + .BeEmpty(); + } + + [TestMethod] + public void BindSourceFolderRows_NullViewer_LeavesFolderRowsUnchanged() + { + // Arrange + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: true); + SetPrivateField(controller, "_formViewer", null); + object prior = GetPrivateField(controller, "_folderRows"); + + // Act + InvokeIssue1002Bind(controller, "BindSourceFolderRows", new[] { "Other" }); + + // Assert + GetPrivateField(controller, "_folderRows").Should().BeSameAs(prior); + documents.Should().BeEmpty("a null viewer delivers no document"); + } + + [TestMethod] + public void BindSourceFolderRows_NullRouter_LeavesFolderRowsUnchanged() + { + // Arrange + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: true); + SetPrivateField(controller, "_router", null); + object prior = GetPrivateField(controller, "_folderRows"); + + // Act + InvokeIssue1002Bind(controller, "BindSourceFolderRows", new[] { "Other" }); + + // Assert + GetPrivateField(controller, "_folderRows").Should().BeSameAs(prior); + documents.Should().BeEmpty("a null router delivers no document"); + } + + [TestMethod] + public async Task BindFolderRows_NullRows_BindsEmptyDocumentWithoutRetaining() + { + // Arrange + var documents = new List(); + var delivered = new TaskCompletionSource(); + EfcFormController controller = CreateIssue1002Controller( + documents, + withGlobals: true, + delivered: delivered + ); + object prior = GetPrivateField(controller, "_folderRows"); + + // Act + InvokeIssue1002Bind(controller, "BindFolderRows", null); + await delivered.Task; + + // Assert + documents.Should().ContainSingle(); + GetPrivateField(controller, "_folderRows").Should().BeSameAs(prior); + } + + [TestMethod] + public void BindFolderRows_NullViewer_DoesNotBind() + { + // Arrange + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: true); + SetPrivateField(controller, "_formViewer", null); + + // Act + Action act = () => + InvokeIssue1002Bind(controller, "BindFolderRows", new[] { Issue1002Presented }); + + // Assert + act.Should().NotThrow(); + documents.Should().BeEmpty("a null viewer delivers no document"); + } + + [TestMethod] + public void BindFolderRows_NullRouter_DoesNotBind() + { + // Arrange + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: true); + SetPrivateField(controller, "_router", null); + + // Act + Action act = () => + InvokeIssue1002Bind(controller, "BindFolderRows", new[] { Issue1002Presented }); + + // Assert + act.Should().NotThrow(); + documents.Should().BeEmpty("a null router delivers no document"); + } + + [TestMethod] + public async Task ApplyDeleteGestureAsync_NullViewer_RetainsTrashRowAndDoesNotBind() + { + // Arrange + var documents = new List(); + EfcFormController controller = CreateIssue1002Controller(documents, withGlobals: true); + SetPrivateField(controller, "_formViewer", null); + + // Act + Func act = () => controller.ApplyDeleteGestureAsync(); + + // Assert + await act.Should().NotThrowAsync("a null viewer returns before binding"); + var folderRows = (string[])GetPrivateField(controller, "_folderRows"); + folderRows.Count(row => row == EfcFormController.TrashRowText).Should().Be(1); + documents.Should().BeEmpty("a null viewer delivers no document"); + } + + [TestMethod] + public void BindFolderRows_FaultBeforeDelivery_FaultsDeliveredTaskPromptly() + { + // Arrange: with _globals unset the bind boundary faults before it delivers a document. + var documents = new List(); + var delivered = new TaskCompletionSource(); + EfcFormController controller = CreateIssue1002Controller( + documents, + withGlobals: false, + delivered: delivered + ); + + // Act + InvokeIssue1002Bind(controller, "BindFolderRows", new[] { Issue1002Presented }); + + // Assert: the fault is reported synchronously, so no wait is needed. + delivered + .Task.IsFaulted.Should() + .BeTrue("a pre-delivery fault must fail the wait instead of leaving it pending"); + delivered.Task.Exception.Should().NotBeNull(); + documents.Should().BeEmpty("no document is delivered by a faulted bind"); + } + + private static void InvokeIssue1002Bind( + EfcFormController controller, + string memberName, + string[] rows + ) + { + MethodInfo member = typeof(EfcFormController).GetMethod( + memberName, + BindingFlags.Instance | BindingFlags.NonPublic + ); + member.Should().NotBeNull($"{memberName} must remain available for this headless seam"); + member.Invoke(controller, new object[] { rows }); + } + + private static EfcFormController CreateIssue1002Controller( + List documents, + bool withGlobals, + TaskCompletionSource delivered = null + ) + { + var provider = new Mock(MockBehavior.Strict); + var host = new Mock(MockBehavior.Strict); + host.SetupGet(value => value.IsCoreInitialized).Returns(true); + host.Setup(value => value.NavigateToString(It.IsAny())) + .Callback(document => + { + documents.Add(document); + delivered?.TrySetResult(true); + }); + host.Setup(value => value.PostMessageJson(It.IsAny())); + provider + .Setup(value => + value.ResolveLeafKeyAsync( + Issue1002ArchiveRoot + @"\" + Issue1002Presented, + It.IsAny() + ) + ) + .ReturnsAsync((FolderTreeNodeKey)null); + var router = new BreadcrumbBridgeRouter( + provider.Object, + host.Object, + new BreadcrumbMessageCodec(), + new BreadcrumbHtmlRenderer(), + new BreadcrumbOutboundQueue(host.Object) + ); + + EfcFormController controller = CreateMinimalController(); + // A fault reported at the bind boundary fails the caller's wait instead of leaving it pending. + controller.BoundaryErrorSink = (message, exception) => + delivered?.TrySetException(exception); + var viewer = (EfcViewer) + System.Runtime.Serialization.FormatterServices.GetUninitializedObject( + typeof(EfcViewer) + ); + SetPrivateField(viewer, "_context", new SynchronizationContext()); + SetPrivateField(controller, "_formViewer", viewer); + SetPrivateField(controller, "_router", router); + SetPrivateField(controller, "_folderRows", new[] { Issue1002Presented }); + if (withGlobals) + { + var ol = new Mock(MockBehavior.Strict); + var globals = new Mock(MockBehavior.Strict); + ol.SetupGet(value => value.ArchiveRootPath).Returns(Issue1002ArchiveRoot); + globals.SetupGet(value => value.Ol).Returns(ol.Object); + SetPrivateField(controller, "_globals", globals.Object); + } + + return controller; + } + } +} diff --git a/QuickFiler.Test/QuickFiler.Test.csproj b/QuickFiler.Test/QuickFiler.Test.csproj index 06feb3957..3ebdaad51 100644 --- a/QuickFiler.Test/QuickFiler.Test.csproj +++ b/QuickFiler.Test/QuickFiler.Test.csproj @@ -67,6 +67,9 @@ + + + @@ -112,6 +115,7 @@ + @@ -132,6 +136,7 @@ + diff --git a/QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs b/QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs new file mode 100644 index 000000000..71cab7046 --- /dev/null +++ b/QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs @@ -0,0 +1,101 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using QuickFiler.Viewers; + +namespace QuickFiler.Test.Viewers +{ + /// + /// Issue 1002 CI-visible contract over the composed QFC selector document: inside + /// makeSubfolders the name is appended before the toggle, and each nested level + /// carries its depth and a per-depth left padding. CI runs no node suite, so these text + /// contracts are the CI signal for the JavaScript change. + /// + [TestClass] + public sealed class FolderBreadcrumbNestingIssue1002Tests + { + [TestMethod] + public void MakeSubfolders_AppendsNameBeforeToggle() + { + // Arrange + string body = MakeSubfoldersBody(); + + // Act + int name = body.IndexOf("subfolderElement.appendChild(name)", StringComparison.Ordinal); + int toggle = body.IndexOf( + "subfolderElement.appendChild(toggle)", + StringComparison.Ordinal + ); + + // Assert + name.Should().BeGreaterThan(-1, "the name is appended to the row"); + toggle.Should().BeGreaterThan(-1, "the toggle is appended to the row"); + name.Should().BeLessThan(toggle, "the +/- follows the folder name"); + } + + [TestMethod] + public void MakeSubfolders_RecursiveCallPassesDepthPlusOne() + { + // Arrange + string body = MakeSubfoldersBody(); + + // Act + bool passesDepth = Regex.IsMatch( + body, + @"makeSubfolders\(\s*row\s*,\s*child\.children\s*,\s*selectedChildOwnsActive\s*,\s*depth\s*\+\s*1\s*\)" + ); + + // Assert + passesDepth.Should().BeTrue("each nested level is one step deeper than its parent"); + } + + [TestMethod] + public void MakeSubfolders_SetsDataDepthAndPerDepthPadding() + { + // Arrange + string body = MakeSubfoldersBody(); + + // Act / Assert + body.Should().Contain("\"data-depth\"", "each row carries its depth"); + body.Should().Contain("padding-left: ", "the indent is padding on the row itself"); + body.Should().Contain("24 + 16 * depth", "one 16px step per level from a 24px base"); + } + + [TestMethod] + public void MakeSubfolders_PinnedRecursiveCallPrefixPreserved() + { + // Arrange + string body = MakeSubfoldersBody(); + + // Act + string[] declared = Regex + .Matches(body, @"function\s+([A-Za-z_]\w*)\s*\(") + .Cast() + .Select(match => match.Groups[1].Value) + .ToArray(); + + // Assert + body.Should().Contain("makeSubfolders(row, child.children", "the #997 pin survives"); + declared + .Should() + .BeEquivalentTo( + new[] { "toggleDescendant", "activateSubfolder" }, + "no function is inserted between makeSubfolders and render" + ); + } + + // The makeSubfolders body runs from its declaration to the next declaration, render. + private static string MakeSubfoldersBody() + { + string html = FolderBreadcrumbDocument.Html; + const string header = "function makeSubfolders("; + int start = html.IndexOf(header, StringComparison.Ordinal); + int end = html.IndexOf("function render", StringComparison.Ordinal); + start.Should().BeGreaterThan(-1, "makeSubfolders is declared"); + end.Should().BeGreaterThan(start, "render follows makeSubfolders"); + return html.Substring(start + header.Length, end - start - header.Length); + } + } +} diff --git a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs index a0afbfd51..408eb6b08 100644 --- a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs +++ b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs @@ -184,10 +184,21 @@ private async Task ExpandLeafAsync(BreadcrumbRow row) IReadOnlyList children = await _provider.GetImmediateSubfoldersAsync(key, CancellationToken.None); + + // #1002 I2: row ids are positional and reused across binds, so a completion that + // arrives after a rebind must not mutate or render the replaced row. + if (!ReferenceEquals(FindRow(row.RowId), row)) + { + log.Error( + $"Breadcrumb expand {requestId} completed after row '{row.RowId}' was replaced; result discarded." + ); + return; + } + IReadOnlyList mapped = BreadcrumbRowBuilder.MapSegments( children ); - row.SetLeafChildren(mapped); + row.SetLeafDescendants(children ?? Array.Empty()); row.ToggleLeafExpanded(); PostOutbound(new BreadcrumbSubfolderResultMessage(requestId, row.RowId, mapped)); PostRowRender(row); diff --git a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs new file mode 100644 index 000000000..5b910b0d3 --- /dev/null +++ b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs @@ -0,0 +1,104 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using UtilitiesCS.OutlookObjects.Folder; + +namespace QuickFiler.Controllers +{ + /// + /// Descendant toggle and activation handlers of the breadcrumb bridge router (#1002). Every + /// request is addressed by a host-issued node identity that must belong to the addressed + /// row's current tree (I1); a fetch result is applied only to the same row instance and the + /// same request generation that started it (I2); a toggle never changes the selection (I3); + /// and an activation commits only through SelectHierarchyPath (I4). Awaits resume on + /// the captured UI context, so no lock is taken and the provider await keeps the default + /// context capture (I6). + /// + public sealed partial class BreadcrumbBridgeRouter + { + private async Task HandleDescendantToggleAsync(BreadcrumbRow row, string nodeId) + { + if (row.TryCollapseDescendant(nodeId)) + { + PostRowRender(row); + return; + } + + if ( + !row.TryBeginDescendantExpand( + nodeId, + out FolderTreeNodeKey? key, + out int generation + ) + ) + { + log.Error( + $"Breadcrumb descendant toggle rejected for row '{row.RowId}': node '{nodeId}' is unknown or has no children." + ); + return; + } + + IReadOnlyList children; + try + { + // Null-forgiving: TryBeginDescendantExpand returns true only with the node's key, + // and net48 has no NotNullWhen attribute to carry that guarantee. + children = await _provider.GetImmediateSubfoldersAsync( + key!, + CancellationToken.None + ); + } + catch (OperationCanceledException) + { + log.Error( + $"Breadcrumb descendant fetch canceled for row '{row.RowId}' node '{nodeId}'; state unchanged." + ); + row.TryAbandonDescendantExpand(nodeId, generation); + return; + } + catch (Exception ex) + { + // Provider I/O boundary: log the specific failure and permit a later retry. + log.Error( + $"Breadcrumb descendant fetch failed for row '{row.RowId}' node '{nodeId}': {ex.Message}", + ex + ); + row.TryAbandonDescendantExpand(nodeId, generation); + return; + } + + if ( + !ReferenceEquals(FindRow(row.RowId), row) + || !row.TryAttachDescendantChildren( + nodeId, + generation, + children ?? Array.Empty() + ) + ) + { + log.Error( + $"Breadcrumb descendant fetch for row '{row.RowId}' node '{nodeId}' was superseded; result discarded." + ); + return; + } + + PostRowRender(row); + } + + private void ActivateDescendant(BreadcrumbRow row, string nodeId) + { + string? path = row.GetDescendantPath(nodeId); + if (path == null) + { + log.Error( + $"Breadcrumb descendant activation rejected for row '{row.RowId}': node '{nodeId}' is unknown." + ); + return; + } + + SelectHierarchyPath(row, path, nodeId); + } + } +} diff --git a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs index a2fbfe1aa..eb6d2d71c 100644 --- a/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs +++ b/QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs @@ -82,48 +82,73 @@ CancellationToken cancellationToken private void SelectRow(BreadcrumbRow row) { + if (!TryGetRowSelection(row, out string selection)) + { + return; + } + + row.TrySelectDescendant(null); // A whole-row selection highlights no descendant. + CommitSelection(row, selection); + } + + /// + /// Computes the filing value a whole-row selection commits: the trash text or the + /// presented filing target, with eligible rooted targets normalized to the archive-relative + /// stem (#614 D2). Shared by and the bind-time preferred row (#1002). + /// + /// False for a banner row and for the two archive-root rejections. + private bool TryGetRowSelection(BreadcrumbRow row, out string selection) + { + selection = string.Empty; if (row.Kind == BreadcrumbRowKind.Banner) { - return; // Banner rows are never selectable. + return false; // Banner rows are never selectable. } - string selection = + string value = row.Kind == BreadcrumbRowKind.TrashPseudoRow ? BreadcrumbRowBuilder.TrashRowText : row.FilingTarget; // #614 D2: normalize eligible rooted targets, preserving no-bound-root pass-through. - if (_boundRoot.Length != 0 && ArchiveStemContract.IsFullOutlookPath(selection)) - { - if ( - !ArchiveStemContract.TryMakeArchiveRelative( - selection, - _boundRoot, - out string stem - ) - ) + if (_boundRoot.Length != 0 && ArchiveStemContract.IsFullOutlookPath(value)) + { + if (!ArchiveStemContract.TryMakeArchiveRelative(value, _boundRoot, out string stem)) { log.Error("Breadcrumb row rejected: target is outside the archive root."); - return; + return false; } if (stem.Length == 0) { log.Error("Breadcrumb row rejected: target is the archive root itself."); - return; + return false; } - selection = stem; + value = stem; } - CommitSelection(row, selection); + selection = value; + return true; } - private void SelectHierarchyPath(BreadcrumbRow row, string fullPath) + /// + /// Commits a hierarchy path under the archive-relative contract. The descendant highlight + /// (#1002) is applied only immediately before the commit, so a rejected path leaves both + /// the prior selection and the prior highlight unchanged. + /// + /// True when the selection was committed. + private bool SelectHierarchyPath( + BreadcrumbRow row, + string fullPath, + string? descendantId = null + ) { if (_boundRoot.Length == 0) { - CommitSelection(row, fullPath); // Preserved no-archive-root binding mode. - return; + // Preserved no-archive-root binding mode. + row.TrySelectDescendant(descendantId); + CommitSelection(row, fullPath); + return true; } // #614 D1/D9: a path outside the archive root, and the root itself, are deterministic @@ -134,10 +159,12 @@ private void SelectHierarchyPath(BreadcrumbRow row, string fullPath) ) { log.Error("Breadcrumb selection rejected: not a folder inside the archive root."); - return; + return false; } + row.TrySelectDescendant(descendantId); CommitSelection(row, stem); + return true; } private void CommitSelection(BreadcrumbRow row, string selection) @@ -150,6 +177,48 @@ private void CommitSelection(BreadcrumbRow row, string selection) SelectedFolderPathChanged?.Invoke(this, SelectedFolderPath); } + /// + /// #1002 I5: selects the row presenting after a bind, + /// before the document is delivered, so the delivered document already marks it. The + /// trash pseudo-row presents the trash text and a suggestion presents its filing target + /// (ordinal match). Posts no outbound message; raises the change event once. + /// + private void SelectPreferredRow(string? preferredRowText) + { + if (string.IsNullOrEmpty(preferredRowText)) + { + return; + } + + foreach (BreadcrumbRow row in _rows) + { + if (!PresentsText(row, preferredRowText!)) + { + continue; + } + + if (!TryGetRowSelection(row, out string selection)) + { + return; + } + + _selectedRowId = row.RowId; + SelectedFolderPath = selection; + SelectedFolderPathChanged?.Invoke(this, SelectedFolderPath); + return; + } + } + + private static bool PresentsText(BreadcrumbRow row, string text) + { + string presented = + row.Kind == BreadcrumbRowKind.TrashPseudoRow + ? BreadcrumbRowBuilder.TrashRowText + : row.FilingTarget; + return row.Kind != BreadcrumbRowKind.Banner + && string.Equals(presented, text, StringComparison.Ordinal); + } + private void ConfirmRow(BreadcrumbRow row) { if ( diff --git a/QuickFiler/Controllers/BreadcrumbBridgeRouter.cs b/QuickFiler/Controllers/BreadcrumbBridgeRouter.cs index 311a0516f..f59f9f146 100644 --- a/QuickFiler/Controllers/BreadcrumbBridgeRouter.cs +++ b/QuickFiler/Controllers/BreadcrumbBridgeRouter.cs @@ -99,11 +99,16 @@ CancellationToken cancellationToken /// Score projections joined by presented filing-target equality. /// The full Outlook archive root used by the hierarchy provider. /// Token observed by the provider calls. + /// + /// Presented text of a row to select inside the delivered document (#1002), or null to + /// leave the rebuilt rows unselected as before. + /// internal async Task BindRowsAsync( IReadOnlyList presentedRows, IEnumerable scores, string archiveRootPath, - CancellationToken cancellationToken + CancellationToken cancellationToken, + string? preferredRowText = null ) { if (presentedRows == null) @@ -176,6 +181,7 @@ CancellationToken cancellationToken SelectedFolderPathChanged?.Invoke(this, null); } + SelectPreferredRow(preferredRowText); DeliverDocument(); } @@ -440,6 +446,12 @@ public async Task ProcessInboundAsync(string json) case BreadcrumbMessageTypes.RowConfirmed: ConfirmRow(row); break; + case BreadcrumbMessageTypes.DescendantToggle: + await HandleDescendantToggleAsync(row, message.NodeId!); + break; + case BreadcrumbMessageTypes.DescendantActivate: + ActivateDescendant(row, message.NodeId!); + break; } } diff --git a/QuickFiler/Controllers/EfcFormController.Actions.cs b/QuickFiler/Controllers/EfcFormController.Actions.cs index 921f3f6dc..b28030f13 100644 --- a/QuickFiler/Controllers/EfcFormController.Actions.cs +++ b/QuickFiler/Controllers/EfcFormController.Actions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; @@ -27,7 +28,10 @@ internal partial class EfcFormController { #region Major Actions - async public Task ActionOkAsync() + // Coverage exemption (#1002, maintainer-ratified CLAUDE.md UT2 COM/VSTO/WinForms exemption): + // this handler is bound to MessageBox and the live WinForms viewer and cannot run headless. + [ExcludeFromCodeCoverage] + public async Task ActionOkAsync() { if (SynchronizationContext.Current is null) SynchronizationContext.SetSynchronizationContext(_formViewer.UiSyncContext); @@ -63,6 +67,9 @@ selectedFolder is null } } + // Coverage exemption (#1002, maintainer-ratified CLAUDE.md UT2 COM/VSTO/WinForms exemption): + // this handler awaits the concrete EfcViewer UiSyncContext and calls Close on the live WinForms viewer, which cannot run headless. + [ExcludeFromCodeCoverage] public async Task ActionCancelAsync() { //Debug.WriteLine($"Thread Id before await: {Thread.CurrentThread.ManagedThreadId}"); @@ -91,19 +98,31 @@ internal static string[] WithTrashRow(string[] rows) return itemList.ToArray(); } - /// Retains the delete-gesture rows, then binds them. - internal void ApplyDeleteGesture() + /// + /// Retains the delete-gesture rows, then awaits a bind that selects the trash row inside + /// the delivered document (#1002). The bind runs through + /// so a bind fault is still reported at that boundary. + /// + internal async Task ApplyDeleteGestureAsync() { _folderRows = WithTrashRow(_folderRows); - BindFolderRows(_folderRows); + if (_formViewer == null || _router == null) + { + return; + } + + await BindBreadcrumbRowsAsync(_folderRows, TrashRowText); } public async Task ActionDeleteAsync() { await _formViewer.UiSyncContext; - ApplyDeleteGesture(); + await ApplyDeleteGestureAsync(); } + // Coverage exemption (#1002, maintainer-ratified CLAUDE.md UT2 COM/VSTO/WinForms exemption): + // this handler depends on MessageBox and the Outlook-bound folder helper and cannot run headless. + [ExcludeFromCodeCoverage] public async Task CreateFolderAsync() { if (!IsValidSelection) @@ -166,6 +185,9 @@ string searchText /// #465 B (RC8): the control read happens here, on the UI thread, before any /// Task.Run, carrying an unchanged value into the worker. /// + // Coverage exemption (#1002, maintainer-ratified CLAUDE.md UT2 COM/VSTO/WinForms exemption): + // this refresh reads the live WinForms search box and the Outlook-bound data model. + [ExcludeFromCodeCoverage] public async Task RefreshSuggestionsAsync() { var searchText = _formViewer.SearchText.Text; diff --git a/QuickFiler/Controllers/EfcFormController.Breadcrumb.cs b/QuickFiler/Controllers/EfcFormController.Breadcrumb.cs index bd807b0af..1852b6c79 100644 --- a/QuickFiler/Controllers/EfcFormController.Breadcrumb.cs +++ b/QuickFiler/Controllers/EfcFormController.Breadcrumb.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Drawing2D; using System.IO; @@ -66,6 +67,9 @@ private void DetachBreadcrumbConfirmation() // Wiring-only breadcrumb setup (#349): constructs the exempt WebView2 host adapter and the // non-exempt router where ConfigureFolderTreeView previously wired the TreeListView, then // connects the router's events back to the form. All breadcrumb logic lives in the router. + // Coverage exemption (#1002, maintainer-ratified CLAUDE.md UT2 COM/VSTO/WinForms exemption): + // this wiring constructs WebView2BreadcrumbHost over the live viewer control and cannot run headless. + [ExcludeFromCodeCoverage] private void ConfigureBreadcrumbControl() { _breadcrumbHost = new WebView2BreadcrumbHost( @@ -186,14 +190,21 @@ private void BindSourceFolderRows(string[] rows) } // Async bind boundary: joins the feature-324 score projection and delegates to the router. - internal async Task BindBreadcrumbRowsAsync(string[] rows) + // preferredRowText (#1002) names a presented row to select inside the bound document. + internal async Task BindBreadcrumbRowsAsync(string[] rows, string preferredRowText = null) { try { var scores = _dataModel?.FolderHelper?.Suggestions?.ToScoredArray() ?? Array.Empty(); - await _router.BindRowsAsync(rows, scores, _globals.Ol.ArchiveRootPath, Token); + await _router.BindRowsAsync( + rows, + scores, + _globals.Ol.ArchiveRootPath, + Token, + preferredRowText + ); } catch (OperationCanceledException) { diff --git a/QuickFiler/QuickFiler.csproj b/QuickFiler/QuickFiler.csproj index 4dee53552..97f7a49f0 100644 --- a/QuickFiler/QuickFiler.csproj +++ b/QuickFiler/QuickFiler.csproj @@ -305,6 +305,7 @@ + diff --git a/QuickFiler/Resources/FolderBreadcrumb.js b/QuickFiler/Resources/FolderBreadcrumb.js index 90466960d..a95d21c39 100644 --- a/QuickFiler/Resources/FolderBreadcrumb.js +++ b/QuickFiler/Resources/FolderBreadcrumb.js @@ -142,7 +142,8 @@ return rowElement; } - function makeSubfolders(row, children, selectedChildOwnsActive) { + function makeSubfolders(row, children, selectedChildOwnsActive, depth) { + depth = depth || 0; var subs = document.createElement("div"); subs.className = "subs"; children.forEach(function (child, index) { @@ -152,11 +153,14 @@ subfolderElement.className = active ? "sub selected active" : "sub"; subfolderElement.setAttribute("role", "option"); subfolderElement.setAttribute("aria-selected", String(active)); + subfolderElement.setAttribute("data-depth", String(depth)); + subfolderElement.setAttribute("style", "padding-left: " + (24 + 16 * depth) + "px"); subfolderElement.tabIndex = 0; var name = document.createElement("span"); name.className = "seg"; name.textContent = child.displayName; name.title = child.folderPath; + subfolderElement.appendChild(name); if (child.hasChildren) { var toggle = document.createElement("span"); toggle.className = "aff"; @@ -176,7 +180,6 @@ }); subfolderElement.appendChild(toggle); } - subfolderElement.appendChild(name); function activateSubfolder(event) { event.preventDefault(); @@ -196,7 +199,7 @@ }); subs.appendChild(subfolderElement); if (child.expanded && Array.isArray(child.children)) { - subs.appendChild(makeSubfolders(row, child.children, selectedChildOwnsActive)); + subs.appendChild(makeSubfolders(row, child.children, selectedChildOwnsActive, depth + 1)); } }); return subs; diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs new file mode 100644 index 000000000..d522ec534 --- /dev/null +++ b/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs @@ -0,0 +1,368 @@ +#nullable enable +using System.Linq; +using System.Text.RegularExpressions; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UtilitiesCS.OutlookObjects.Folder; + +namespace UtilitiesCS.Test.OutlookObjects.Folder +{ + /// + /// Issue 1002 rendering and bridge-asset contracts: recursive keyed children with the node + /// affordance after the name, sibling nesting, aria-selected marking, encoding, the unchanged + /// key-less markup, and the text order of the bridge script branches (no browser engine runs + /// in CI, so the script order is asserted as text). + /// + [TestClass] + public class BreadcrumbHtmlRendererIssue1002Tests + { + private const string North = @"\Archive\Clients\North"; + private const string Q3 = North + @"\Q3"; + private const string Q4 = North + @"\Q4"; + private const string NodeAffordance = "Q3", "the name span is rendered"); + node.IndexOf(">Q3", System.StringComparison.Ordinal) + .Should() + .BeLessThan( + node.IndexOf(NodeAffordance, System.StringComparison.Ordinal), + "the node affordance follows the name" + ); + node.Should() + .Contain( + NodeAffordance + " role=\"button\" tabindex=\"0\" aria-expanded=\"false\">+" + ); + } + + [TestMethod] + public void RenderRowFragment_KeyedLeafChild_RendersNoAffordance() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q4 = IdOf(row, Q4); + + // Act + string html = _renderer.RenderRowFragment(row, false); + + // Assert + ChildElement(html, q4).Should().NotContain("data-role=\"node\"", "Q4 has no children"); + Regex.Matches(html, "data-role=\"node\"").Count.Should().Be(1, "only Q3 is expandable"); + } + + [TestMethod] + public void RenderRowFragment_ExpandedDescendant_NestsChildrenContainerAfterNode() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + row.TryBeginDescendantExpand(q3, out _, out int gen).Should().BeTrue(); + row.TryAttachDescendantChildren(q3, gen, SecondLevel()).Should().BeTrue(); + string invoices = row.DescendantNodes[0].Children[0].Identity; + + // Act + string html = _renderer.RenderRowFragment(row, false); + + // Assert + string q3Element = ChildElement(html, q3); + q3Element.Should().Contain("aria-expanded=\"true\">−"); + int q3End = html.IndexOf(q3Element, System.StringComparison.Ordinal) + q3Element.Length; + html.Substring(q3End) + .Should() + .StartWith( + "
&", "A&", false) }) + .Should() + .BeTrue(); + row.ToggleLeafExpanded().Should().BeTrue(); + + // Act + string html = _renderer.RenderRowFragment(row, false); + + // Assert + html.Should().Contain(">A<b>&"); + html.Should().Contain("title=\"" + North + @"\A<b>&" + "\""); + html.Should().NotContain("A", "provider names are never emitted as markup"); + } + + [TestMethod] + public void RenderRowFragment_KeylessChildren_RenderUnchanged() + { + // Arrange + BreadcrumbRow row = ExpandableRow(); + row.SetLeafChildren(new[] { new BreadcrumbSegment(Q3, "Q3", true) }).Should().BeTrue(); + row.ToggleLeafExpanded().Should().BeTrue(); + + // Act + string html = _renderer.RenderRowFragment(row, false); + + // Assert + html.Should() + .Contain( + "
Q3
" + ); + html.Should().NotContain("data-node-id"); + } + + [TestMethod] + public void BridgeJs_NodeAffordanceBranch_PrecedesChildAndLeafBranches() + { + // Arrange + string js = BreadcrumbDocumentAssets.BridgeJs; + int nodeBranch = js.IndexOf( + ".affordance[data-role=\"node\"]", + System.StringComparison.Ordinal + ); + + // Act + int keyedChild = js.IndexOf(".child[data-node-id]", System.StringComparison.Ordinal); + int keylessChild = js.IndexOf( + ".child[data-child-index]", + System.StringComparison.Ordinal + ); + int leafToggle = js.IndexOf( + "type: 'leafExpandToggle'", + System.StringComparison.Ordinal + ); + + // Assert + nodeBranch.Should().BeGreaterThan(-1, "the node affordance branch exists"); + nodeBranch.Should().BeLessThan(keyedChild, "the affordance sits inside the child"); + keyedChild.Should().BeLessThan(keylessChild); + keylessChild.Should().BeLessThan(leafToggle); + js.Should().Contain("type: 'descendantToggle'"); + } + + [TestMethod] + public void BridgeJs_DescendantActivateBranch_PrecedesKeylessChildBranch() + { + // Arrange + string js = BreadcrumbDocumentAssets.BridgeJs; + + // Act + int keyedChild = js.IndexOf(".child[data-node-id]", System.StringComparison.Ordinal); + int activate = js.IndexOf( + "type: 'descendantActivate'", + System.StringComparison.Ordinal + ); + int keylessChild = js.IndexOf( + ".child[data-child-index]", + System.StringComparison.Ordinal + ); + + // Assert + keyedChild.Should().BeGreaterThan(-1, "the keyed child branch exists"); + activate.Should().BeGreaterThan(keyedChild); + activate.Should().BeLessThan(keylessChild, "the key-less branch is retained after it"); + js.Should().Contain("type: 'renderedChildActivate'"); + } + + [TestMethod] + public void BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan() + { + // Arrange + string js = BreadcrumbDocumentAssets.BridgeJs; + int handler = js.IndexOf( + "msg.type === 'subfolderResult'", + System.StringComparison.Ordinal + ); + + // Act + string tail = handler < 0 ? string.Empty : js.Substring(handler); + + // Assert + handler.Should().BeGreaterThan(-1, "the subfolderResult branch exists"); + tail.Should().Contain("name.className = 'name';"); + tail.Should().Contain("name.textContent = c.displayName;"); + tail.Should().Contain("div.appendChild(name);"); + tail.Should() + .NotContain( + "div.textContent", + "bare text would bypass the .child .name truncation" + ); + } + + [TestMethod] + public void BridgeJs_NodeAffordanceEnterOrSpace_PrecedesRowConfirmation() + { + // Arrange + string js = BreadcrumbDocumentAssets.BridgeJs; + int keydown = js.IndexOf("addEventListener('keydown'", System.StringComparison.Ordinal); + string listener = js.Substring(keydown); + + // Act + int space = listener.IndexOf("e.key === ' '", System.StringComparison.Ordinal); + int confirm = listener.IndexOf("type: 'rowConfirmed'", System.StringComparison.Ordinal); + int toggle = listener.IndexOf( + "type: 'descendantToggle'", + System.StringComparison.Ordinal + ); + + // Assert + keydown.Should().BeGreaterThan(-1); + space.Should().BeGreaterThan(-1, "Space toggles a focused node affordance"); + space.Should().BeLessThan(confirm, "toggling by keyboard never confirms the row"); + toggle.Should().BeGreaterThan(-1).And.BeLessThan(confirm); + } + + [TestMethod] + public void BaseCss_Child_IsFlexRow() + { + // Arrange + string css = BreadcrumbDocumentAssets.BaseCss; + + // Act + Match rule = Regex.Match(css, @"(?m)^\.child \{[^}]*\}"); + + // Assert + rule.Success.Should().BeTrue("the child rule exists"); + rule.Value.Should().Contain("display: flex").And.Contain("align-items: center"); + Regex.Matches(css, @"(?m)^\.child \{").Count.Should().Be(1, "the rule is merged"); + } + + [TestMethod] + public void LightThemeCss_SelectedChild_UsesSelectionBackground() + { + // Arrange + string css = BreadcrumbDocumentAssets.LightThemeCss; + + // Act / Assert + css.Should() + .Contain(".child[aria-selected=\"true\"] { background: #cce8ff; }") + .And.Contain(".rowwrap.selected .row { background: #cce8ff; }"); + } + + [TestMethod] + public void DarkThemeCss_SelectedChild_UsesSelectionBackground() + { + // Arrange + string css = BreadcrumbDocumentAssets.DarkThemeCss; + + // Act / Assert + css.Should() + .Contain(".child[aria-selected=\"true\"] { background: #264f78; }") + .And.Contain(".rowwrap.selected .row { background: #264f78; }"); + } + + [TestMethod] + public void BaseCss_ChildName_CarriesEllipsisRule() + { + // Arrange + string css = BreadcrumbDocumentAssets.BaseCss; + + // Act + Match nameRule = Regex.Match(css, @"(?m)^\.child \.name \{[^}]*\}"); + Match childRule = Regex.Match(css, @"(?m)^\.child \{[^}]*\}"); + + // Assert + nameRule.Success.Should().BeTrue("the name rule exists"); + nameRule + .Value.Should() + .Contain("min-width: 0") + .And.Contain("overflow: hidden") + .And.Contain("text-overflow: ellipsis") + .And.Contain("white-space: nowrap"); + childRule.Value.Should().NotContain("text-overflow"); + childRule.Value.Should().Contain("display: flex").And.Contain("align-items: center"); + } + + private static BreadcrumbRow ExpandableRow() => + new BreadcrumbRow( + "row-0", + BreadcrumbRowKind.Suggestion, + new[] + { + new BreadcrumbSegment(@"\Archive\Clients", "Clients", true), + new BreadcrumbSegment(North, "North", true), + }, + null + ); + + private static BreadcrumbRow RowWithTree() + { + BreadcrumbRow row = ExpandableRow(); + row.SetLeafDescendants(new[] { Segment(Q3, "Q3", true), Segment(Q4, "Q4", false) }) + .Should() + .BeTrue("the fixture needs a keyed tree"); + row.ToggleLeafExpanded().Should().BeTrue(); + return row; + } + + private static FolderBreadcrumbSegment[] SecondLevel() => + new[] + { + Segment(Q3 + @"\Invoices", "Invoices", false), + Segment(Q3 + @"\Receipts", "Receipts", true), + }; + + private static string IdOf(BreadcrumbRow row, string path) => + row.DescendantNodes.Single(node => node.Segment.FolderPath == path).Identity; + + // The node element runs from its opening tag to its own closing tag (nodes hold no div). + private static string ChildElement(string html, string nodeId) + { + Match match = Regex.Match( + html, + "
]*>.*?
" + ); + match.Success.Should().BeTrue($"node {nodeId} must be rendered"); + return match.Value; + } + + private static FolderTreeNodeKey Key(string path) => + new FolderTreeNodeKey("store", path, path); + + private static FolderBreadcrumbSegment Segment( + string path, + string name, + bool hasChildren + ) => new FolderBreadcrumbSegment(Key(path), name, path, hasChildren); + } +} diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs new file mode 100644 index 000000000..bdb2cdd7d --- /dev/null +++ b/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs @@ -0,0 +1,108 @@ +#nullable enable +using System; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UtilitiesCS.OutlookObjects.Folder; + +namespace UtilitiesCS.Test.OutlookObjects.Folder +{ + /// + /// Issue 1002 codec contract: the descendant toggle and activate inbound types carry a row + /// identity plus a non-empty string node identity, and every malformed node identity fails + /// fast with . + /// + [TestClass] + public class BreadcrumbMessageCodecIssue1002Tests + { + private const string NodeId = "0123456789abcdef0123456789abcdef"; + + private readonly BreadcrumbMessageCodec _codec = new BreadcrumbMessageCodec(); + + [TestMethod] + public void DeserializeInbound_DescendantToggle_ReturnsNodeId() + { + // Arrange + string json = + "{\"type\":\"descendantToggle\",\"rowId\":\"row-0\",\"nodeId\":\"" + NodeId + "\"}"; + + // Act + BreadcrumbInboundMessage message = _codec.DeserializeInbound(json); + + // Assert + message.Type.Should().Be(BreadcrumbMessageTypes.DescendantToggle); + message.RowId.Should().Be("row-0"); + message.NodeId.Should().Be(NodeId, "the node identity is the descendant address"); + message.SegmentIndex.Should().BeNull(); + message.ChildIndex.Should().BeNull(); + } + + [TestMethod] + public void DeserializeInbound_DescendantActivate_ReturnsNodeId() + { + // Arrange + string json = + "{\"type\":\"descendantActivate\",\"rowId\":\"row-2\",\"nodeId\":\"" + + NodeId + + "\"}"; + + // Act + BreadcrumbInboundMessage message = _codec.DeserializeInbound(json); + + // Assert + message.Type.Should().Be(BreadcrumbMessageTypes.DescendantActivate); + message.RowId.Should().Be("row-2"); + message.NodeId.Should().Be(NodeId, "the node identity is the descendant address"); + } + + [DataTestMethod] + [DataRow("descendantToggle")] + [DataRow("descendantActivate")] + public void DeserializeInbound_DescendantMessageWithoutNodeId_Throws(string type) + { + // Arrange + string json = "{\"type\":\"" + type + "\",\"rowId\":\"row-0\"}"; + + // Act + Action act = () => _codec.DeserializeInbound(json); + + // Assert + act.Should() + .Throw() + .WithMessage("*nodeId*", "a missing node identity is reported by field name"); + } + + [DataTestMethod] + [DataRow("descendantToggle")] + [DataRow("descendantActivate")] + public void DeserializeInbound_DescendantMessageWithEmptyNodeId_Throws(string type) + { + // Arrange + string json = "{\"type\":\"" + type + "\",\"rowId\":\"row-0\",\"nodeId\":\"\"}"; + + // Act + Action act = () => _codec.DeserializeInbound(json); + + // Assert + act.Should() + .Throw() + .WithMessage("*nodeId*", "an empty node identity is reported by field name"); + } + + [DataTestMethod] + [DataRow("descendantToggle")] + [DataRow("descendantActivate")] + public void DeserializeInbound_DescendantMessageWithNonStringNodeId_Throws(string type) + { + // Arrange + string json = "{\"type\":\"" + type + "\",\"rowId\":\"row-0\",\"nodeId\":7}"; + + // Act + Action act = () => _codec.DeserializeInbound(json); + + // Assert + act.Should() + .Throw() + .WithMessage("*nodeId*", "a non-string node identity is reported by field name"); + } + } +} diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs new file mode 100644 index 000000000..4c45ee230 --- /dev/null +++ b/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs @@ -0,0 +1,98 @@ +#nullable enable +using System; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UtilitiesCS.OutlookObjects.Folder; + +namespace UtilitiesCS.Test.OutlookObjects.Folder +{ + /// + /// Issue 1002 message constructors: every required argument is guarded and a null argument + /// fails fast with naming the offending parameter. + /// + [TestClass] + public class BreadcrumbMessagesIssue1002Tests + { + private static readonly IReadOnlyList NoChildren = + Array.Empty(); + + [TestMethod] + public void BreadcrumbInboundMessage_NullType_ThrowsWithParamName() + { + // Arrange + Action act = () => _ = new BreadcrumbInboundMessage(null!, "row-0", null, null, null); + + // Act / Assert + act.Should().Throw().And.ParamName.Should().Be("type"); + } + + [TestMethod] + public void BreadcrumbInboundMessage_NullRowId_ThrowsWithParamName() + { + // Arrange + Action act = () => _ = new BreadcrumbInboundMessage("select", null!, null, null, null); + + // Act / Assert + act.Should().Throw().And.ParamName.Should().Be("rowId"); + } + + [TestMethod] + public void BreadcrumbOutboundMessage_NullType_ThrowsWithParamName() + { + // Arrange + Action act = () => _ = new NullTypeOutboundMessage(); + + // Act / Assert + act.Should().Throw().And.ParamName.Should().Be("type"); + } + + [TestMethod] + public void BreadcrumbRenderMessage_NullHtml_ThrowsWithParamName() + { + // Arrange + Action act = () => _ = new BreadcrumbRenderMessage(null!, null); + + // Act / Assert + act.Should().Throw().And.ParamName.Should().Be("html"); + } + + [TestMethod] + public void BreadcrumbSubfolderResultMessage_NullRequestId_Throws() + { + // Arrange + Action act = () => _ = new BreadcrumbSubfolderResultMessage(null!, "row-0", NoChildren); + + // Act / Assert + act.Should().Throw().And.ParamName.Should().Be("requestId"); + } + + [TestMethod] + public void BreadcrumbSubfolderResultMessage_NullRowId_Throws() + { + // Arrange + Action act = () => _ = new BreadcrumbSubfolderResultMessage("req-0", null!, NoChildren); + + // Act / Assert + act.Should().Throw().And.ParamName.Should().Be("rowId"); + } + + [TestMethod] + public void BreadcrumbSubfolderResultMessage_NullChildren_Throws() + { + // Arrange + Action act = () => _ = new BreadcrumbSubfolderResultMessage("req-0", "row-0", null!); + + // Act / Assert + act.Should().Throw().And.ParamName.Should().Be("children"); + } + + // The base constructor is protected and every production subclass passes a constant, so a + // test subclass is the only way to forward a null discriminator. + private sealed class NullTypeOutboundMessage : BreadcrumbOutboundMessage + { + public NullTypeOutboundMessage() + : base(null!) { } + } + } +} diff --git a/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs b/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs new file mode 100644 index 000000000..6f6be8f05 --- /dev/null +++ b/UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs @@ -0,0 +1,466 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UtilitiesCS.OutlookObjects.Folder; + +namespace UtilitiesCS.Test.OutlookObjects.Folder +{ + /// + /// Issue 1002 row state: the keyed descendant tree on , its + /// identity-addressed transitions, the request-generation stale guard, and the reset points + /// shared with the first-level children. + /// + [TestClass] + public class BreadcrumbRowDescendantsIssue1002Tests + { + private const string North = @"\Archive\Clients\North"; + private const string Q3 = North + @"\Q3"; + private const string Q4 = North + @"\Q4"; + private const string UnknownId = "ffffffffffffffffffffffffffffffff"; + + [TestMethod] + public void SetLeafDescendants_KeyedChildren_BuildsOneNodePerChild() + { + // Arrange + BreadcrumbRow row = ExpandableRow(); + + // Act + bool stored = row.SetLeafDescendants(FirstLevel()); + + // Assert + stored.Should().BeTrue("the active leaf has subfolders"); + row.DescendantNodes.Select(node => node.Segment.Key) + .Should() + .Equal(new[] { Key(Q3), Key(Q4) }, "one node per keyed child, in provider order"); + row.LeafChildren.Select(child => child.FullPath) + .Should() + .Equal(new[] { Q3, Q4 }, "the key-less projection stays populated"); + } + + [TestMethod] + public void SetLeafDescendants_DuplicateDisplayNames_KeepsDistinctIdentities() + { + // Arrange + BreadcrumbRow row = ExpandableRow(); + var children = new[] + { + Segment(North + @"\A\Reports", "Reports", false), + Segment(North + @"\B\Reports", "Reports", false), + }; + + // Act + row.SetLeafDescendants(children); + + // Assert + IReadOnlyList nodes = row.DescendantNodes; + nodes.Should().HaveCount(2); + nodes[0].Identity.Should().NotBe(nodes[1].Identity, "identities are per node"); + row.GetDescendantPath(nodes[0].Identity).Should().Be(North + @"\A\Reports"); + row.GetDescendantPath(nodes[1].Identity).Should().Be(North + @"\B\Reports"); + } + + [TestMethod] + public void SetLeafDescendants_RowWithoutExpandableLeaf_ReturnsFalse() + { + // Arrange + var row = new BreadcrumbRow( + "row-0", + BreadcrumbRowKind.Suggestion, + new[] { new BreadcrumbSegment(North, "North", false) }, + null + ); + + // Act + bool stored = row.SetLeafDescendants(FirstLevel()); + + // Assert + stored.Should().BeFalse("a leaf without subfolders cannot hold descendants"); + row.DescendantNodes.Should().BeEmpty(); + } + + [TestMethod] + public void SetLeafDescendants_NullChildren_Throws() + { + // Arrange + BreadcrumbRow row = ExpandableRow(); + + // Act + System.Action act = () => row.SetLeafDescendants(null!); + + // Assert + act.Should().Throw().WithParameterName("children"); + } + + [TestMethod] + public void TryBeginDescendantExpand_KnownNodeWithChildren_ReturnsKeyAndGeneration() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + + // Act + bool begun = row.TryBeginDescendantExpand(q3, out FolderTreeNodeKey? key, out int gen); + + // Assert + begun.Should().BeTrue("Q3 is known and has children"); + key.Should().Be(Key(Q3), "the provider is queried with the node's own key"); + gen.Should().Be(1, "the first request advances the generation from zero"); + row.DescendantNodes[0].Expanded.Should().BeTrue(); + } + + [TestMethod] + public void TryBeginDescendantExpand_UnknownNode_ReturnsFalse() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + + // Act + bool begun = row.TryBeginDescendantExpand(UnknownId, out FolderTreeNodeKey? key, out _); + + // Assert + begun.Should().BeFalse("an identity the row never issued is rejected"); + key.Should().BeNull(); + } + + [TestMethod] + public void TryBeginDescendantExpand_LeafNode_ReturnsFalse() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + + // Act + bool begun = row.TryBeginDescendantExpand(IdOf(row, Q4), out _, out _); + + // Assert + begun.Should().BeFalse("a node without children has nothing to expand"); + row.DescendantNodes[1].Expanded.Should().BeFalse(); + } + + [TestMethod] + public void TryCollapseDescendant_ExpandedNode_CollapsesIt() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + row.TryBeginDescendantExpand(q3, out _, out _).Should().BeTrue(); + + // Act + bool collapsed = row.TryCollapseDescendant(q3); + + // Assert + collapsed.Should().BeTrue(); + row.DescendantNodes[0].Expanded.Should().BeFalse(); + } + + [TestMethod] + public void TryCollapseDescendant_CollapsedNode_ReturnsFalse() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + + // Act + bool collapsed = row.TryCollapseDescendant(IdOf(row, Q3)); + + // Assert + collapsed.Should().BeFalse("a collapsed node is not collapsed again"); + } + + [TestMethod] + public void TryAttachDescendantChildren_MatchingGeneration_AttachesChildren() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + row.TryBeginDescendantExpand(q3, out _, out int gen).Should().BeTrue(); + + // Act + bool attached = row.TryAttachDescendantChildren(q3, gen, SecondLevel()); + + // Assert + attached.Should().BeTrue(); + row.DescendantNodes[0] + .Children.Select(node => node.Segment.FolderPath) + .Should() + .Equal(Q3 + @"\Invoices", Q3 + @"\Receipts"); + } + + [TestMethod] + public void TryAttachDescendantChildren_SupersededGeneration_IsRejected() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + row.TryBeginDescendantExpand(q3, out _, out int first).Should().BeTrue(); + row.TryCollapseDescendant(q3).Should().BeTrue(); + row.TryBeginDescendantExpand(q3, out _, out int second).Should().BeTrue(); + + // Act + bool attached = row.TryAttachDescendantChildren(q3, first, SecondLevel()); + + // Assert + attached.Should().BeFalse("a later request superseded the first generation"); + second.Should().BeGreaterThan(first); + row.DescendantNodes[0].Children.Should().BeEmpty(); + } + + [TestMethod] + public void TryAttachDescendantChildren_CollapsedNode_IsRejected() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + row.TryBeginDescendantExpand(q3, out _, out int gen).Should().BeTrue(); + row.TryCollapseDescendant(q3).Should().BeTrue(); + + // Act + bool attached = row.TryAttachDescendantChildren(q3, gen, SecondLevel()); + + // Assert + attached.Should().BeFalse("the node was collapsed while the fetch was pending"); + row.DescendantNodes[0].Children.Should().BeEmpty(); + } + + [TestMethod] + public void TryAttachDescendantChildren_RemovedNode_IsRejected() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + row.TryBeginDescendantExpand(q3, out _, out int gen).Should().BeTrue(); + row.SetLeafDescendants(FirstLevel()).Should().BeTrue(); + + // Act + bool attached = row.TryAttachDescendantChildren(q3, gen, SecondLevel()); + + // Assert + attached.Should().BeFalse("the rebuilt tree no longer contains the old node"); + } + + [TestMethod] + public void TryAbandonDescendantExpand_CurrentGeneration_CollapsesNode() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + row.TryBeginDescendantExpand(q3, out _, out int gen).Should().BeTrue(); + + // Act + bool abandoned = row.TryAbandonDescendantExpand(q3, gen); + + // Assert + abandoned.Should().BeTrue(); + row.DescendantNodes[0].Expanded.Should().BeFalse("a failed fetch leaves no expansion"); + row.TryBeginDescendantExpand(q3, out _, out _).Should().BeTrue("a retry is allowed"); + } + + [TestMethod] + public void TryAbandonDescendantExpand_SupersededGeneration_LeavesNodeUnchanged() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q3 = IdOf(row, Q3); + row.TryBeginDescendantExpand(q3, out _, out int first).Should().BeTrue(); + row.TryCollapseDescendant(q3).Should().BeTrue(); + row.TryBeginDescendantExpand(q3, out _, out _).Should().BeTrue(); + + // Act + bool abandoned = row.TryAbandonDescendantExpand(q3, first); + + // Assert + abandoned.Should().BeFalse("only the current request may abandon the expansion"); + row.DescendantNodes[0].Expanded.Should().BeTrue(); + } + + [TestMethod] + public void GetDescendantPath_KnownNode_ReturnsFolderPath() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + + // Act + string? path = row.GetDescendantPath(IdOf(row, Q4)); + + // Assert + path.Should().Be(Q4); + } + + [TestMethod] + public void GetDescendantPath_UnknownNode_ReturnsNull() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + + // Act + string? path = row.GetDescendantPath(UnknownId); + + // Assert + path.Should().BeNull("an unknown identity resolves to no path"); + } + + [TestMethod] + public void TrySelectDescendant_KnownNode_SetsSelectedDescendantId() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q4 = IdOf(row, Q4); + + // Act + bool selected = row.TrySelectDescendant(q4); + + // Assert + selected.Should().BeTrue(); + row.SelectedDescendantId.Should().Be(q4); + row.TrySelectDescendant(null).Should().BeTrue("null clears the selection"); + row.SelectedDescendantId.Should().BeNull(); + } + + [TestMethod] + public void TrySelectDescendant_UnknownNode_LeavesSelectionUnchanged() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + string q4 = IdOf(row, Q4); + row.TrySelectDescendant(q4).Should().BeTrue(); + + // Act + bool selected = row.TrySelectDescendant(UnknownId); + + // Assert + selected.Should().BeFalse(); + row.SelectedDescendantId.Should().Be(q4, "a rejected identity keeps the prior one"); + } + + [TestMethod] + public void ActivateSegment_ClearsDescendantsAndSelection() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + row.SetSegmentKey(0, Key(@"\Archive\Clients")); + row.TrySelectDescendant(IdOf(row, Q4)).Should().BeTrue(); + + // Act + bool activated = row.ActivateSegment(0); + + // Assert + activated.Should().BeTrue(); + row.DescendantNodes.Should().BeEmpty("the active segment changed"); + row.SelectedDescendantId.Should().BeNull(); + } + + [TestMethod] + public void CollapseAfter_ClearsDescendantsAndSelection() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + row.TrySelectDescendant(IdOf(row, Q4)).Should().BeTrue(); + + // Act + bool collapsed = row.CollapseAfter(0); + + // Assert + collapsed.Should().BeTrue(); + row.DescendantNodes.Should().BeEmpty("the leaf children are hidden"); + row.SelectedDescendantId.Should().BeNull(); + } + + [TestMethod] + public void LeftArrow_ExpandedLeaf_ClearsDescendantsAndSelection() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + row.TrySelectDescendant(IdOf(row, Q4)).Should().BeTrue(); + + // Act + bool changed = row.LeftArrow(); + + // Assert + changed.Should().BeTrue(); + row.IsLeafExpanded.Should().BeFalse(); + row.DescendantNodes.Should().BeEmpty("the leaf children were collapsed"); + row.SelectedDescendantId.Should().BeNull(); + } + + [TestMethod] + public void ToggleLeafExpanded_Collapse_ClearsDescendantsAndSelection() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + row.TrySelectDescendant(IdOf(row, Q4)).Should().BeTrue(); + + // Act + bool changed = row.ToggleLeafExpanded(); + + // Assert + changed.Should().BeTrue(); + row.IsLeafExpanded.Should().BeFalse(); + row.DescendantNodes.Should().BeEmpty("the leaf children were collapsed"); + row.SelectedDescendantId.Should().BeNull(); + } + + [TestMethod] + public void SetLeafChildren_KeylessChildren_HasNoDescendants() + { + // Arrange + BreadcrumbRow row = RowWithTree(); + + // Act + bool stored = row.SetLeafChildren(new[] { new BreadcrumbSegment(Q3, "Q3", true) }); + + // Assert + stored.Should().BeTrue(); + row.DescendantNodes.Should().BeEmpty("key-less children carry no expandable nodes"); + row.SelectedDescendantId.Should().BeNull(); + } + + private static BreadcrumbRow ExpandableRow() => + new BreadcrumbRow( + "row-0", + BreadcrumbRowKind.Suggestion, + new[] + { + new BreadcrumbSegment(@"\Archive\Clients", "Clients", true), + new BreadcrumbSegment(North, "North", true), + }, + null + ); + + // A row whose expanded leaf holds the keyed first level Q3 (has children) and Q4 (leaf). + private static BreadcrumbRow RowWithTree() + { + BreadcrumbRow row = ExpandableRow(); + row.SetLeafDescendants(FirstLevel()).Should().BeTrue("the fixture needs a keyed tree"); + row.ToggleLeafExpanded().Should().BeTrue(); + return row; + } + + private static string IdOf(BreadcrumbRow row, string path) + { + BreadcrumbDescendantNode? node = row.DescendantNodes.FirstOrDefault(candidate => + candidate.Segment.FolderPath == path + ); + node.Should().NotBeNull($"the keyed tree must contain {path}"); + return node!.Identity; + } + + private static FolderBreadcrumbSegment[] FirstLevel() => + new[] { Segment(Q3, "Q3", true), Segment(Q4, "Q4", false) }; + + private static FolderBreadcrumbSegment[] SecondLevel() => + new[] + { + Segment(Q3 + @"\Invoices", "Invoices", false), + Segment(Q3 + @"\Receipts", "Receipts", true), + }; + + private static FolderTreeNodeKey Key(string path) => + new FolderTreeNodeKey("store", path, path); + + private static FolderBreadcrumbSegment Segment( + string path, + string name, + bool hasChildren + ) => new FolderBreadcrumbSegment(Key(path), name, path, hasChildren); + } +} diff --git a/UtilitiesCS.Test/UtilitiesCS.Test.csproj b/UtilitiesCS.Test/UtilitiesCS.Test.csproj index 6c7d995b3..888312369 100644 --- a/UtilitiesCS.Test/UtilitiesCS.Test.csproj +++ b/UtilitiesCS.Test/UtilitiesCS.Test.csproj @@ -292,6 +292,10 @@ + + + + diff --git a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs index 2c8dad871..c8599ff87 100644 --- a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs +++ b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs @@ -26,18 +26,21 @@ public static class BreadcrumbDocumentAssets + ".affordance { cursor: pointer; font-weight: bold; padding: 0 4px; }\n" + ".row.banner { justify-content: center; opacity: 0.7; pointer-events: none; }\n" + ".children { margin-left: 18px; }\n" - + ".child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n"; + + ".child { display: flex; align-items: center; }\n" + + ".child .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n"; /// Light-theme CSS block. public const string LightThemeCss = "body { background: #ffffff; color: #1b1b1b; }\n" + ".rowwrap.selected .row { background: #cce8ff; }\n" + + ".child[aria-selected=\"true\"] { background: #cce8ff; }\n" + ".affordance { color: #005a9e; }\n"; /// Dark-theme CSS block. public const string DarkThemeCss = "body { background: #1e1e1e; color: #e6e6e6; }\n" + ".rowwrap.selected .row { background: #264f78; }\n" + + ".child[aria-selected=\"true\"] { background: #264f78; }\n" + ".affordance { color: #6cb6ff; }\n"; /// @@ -76,6 +79,25 @@ public static class BreadcrumbDocumentAssets + " e.stopPropagation();\n" + " return;\n" + " }\n" + + " var node = e.target.closest('.affordance[data-role=\"node\"]');\n" + + " if (node) {\n" + + " var nodeOwner = node.closest('[data-node-id]');\n" + + " var nodeRowId = rowIdOf(node);\n" + + " if (nodeRowId !== null && nodeOwner) {\n" + + " post({ type: 'descendantToggle', rowId: nodeRowId, nodeId: nodeOwner.getAttribute('data-node-id') });\n" + + " }\n" + + " e.stopPropagation();\n" + + " return;\n" + + " }\n" + + " var keyed = e.target.closest('.child[data-node-id]');\n" + + " if (keyed) {\n" + + " var keyedRowId = rowIdOf(keyed);\n" + + " if (keyedRowId !== null) {\n" + + " post({ type: 'descendantActivate', rowId: keyedRowId, nodeId: keyed.getAttribute('data-node-id') });\n" + + " }\n" + + " e.stopPropagation();\n" + + " return;\n" + + " }\n" + " var child = e.target.closest('.child[data-child-index]');\n" + " if (child) {\n" + " var childId = rowIdOf(child);\n" @@ -99,6 +121,17 @@ public static class BreadcrumbDocumentAssets + " }\n" + " });\n" + " document.addEventListener('keydown', function (e) {\n" + + " var focusedNode = e.target && e.target.closest ? e.target.closest('.affordance[data-role=\"node\"]') : null;\n" + + " if (focusedNode && (e.key === 'Enter' || e.key === ' ')) {\n" + + " e.preventDefault();\n" + + " e.stopPropagation();\n" + + " var focusedOwner = focusedNode.closest('[data-node-id]');\n" + + " var focusedRowId = rowIdOf(focusedNode);\n" + + " if (focusedRowId !== null && focusedOwner) {\n" + + " post({ type: 'descendantToggle', rowId: focusedRowId, nodeId: focusedOwner.getAttribute('data-node-id') });\n" + + " }\n" + + " return;\n" + + " }\n" + " if (e.key === 'Enter') {\n" + " e.preventDefault();\n" + " e.stopPropagation();\n" @@ -135,7 +168,10 @@ public static class BreadcrumbDocumentAssets + " (msg.children || []).forEach(function (c) {\n" + " var div = document.createElement('div');\n" + " div.className = 'child';\n" - + " div.textContent = c.displayName;\n" + + " var name = document.createElement('span');\n" + + " name.className = 'name';\n" + + " name.textContent = c.displayName;\n" + + " div.appendChild(name);\n" + " div.title = c.fullPath;\n" + " host.appendChild(div);\n" + " });\n" diff --git a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs index 531e2a99e..107c49743 100644 --- a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs +++ b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs @@ -108,7 +108,7 @@ public string RenderRowFragment(BreadcrumbRow row, bool isSelected) AppendTrashRow(sb); break; default: - AppendSuggestionRow(sb, row); + AppendSuggestionRow(sb, row, isSelected); break; } @@ -137,7 +137,11 @@ private static void AppendTrashRow(StringBuilder sb) sb.Append("
"); } - private static void AppendSuggestionRow(StringBuilder sb, BreadcrumbRow row) + private static void AppendSuggestionRow( + StringBuilder sb, + BreadcrumbRow row, + bool isSelected + ) { sb.Append("
"); @@ -169,7 +173,7 @@ private static void AppendSuggestionRow(StringBuilder sb, BreadcrumbRow row) sb.Append("
"); AppendPercent(sb, row.Probability); sb.Append("
"); - AppendChildren(sb, row); + AppendChildren(sb, row, isSelected); } private static void AppendSegment( @@ -210,10 +214,21 @@ private static void AppendPercent(StringBuilder sb, double? probability) .Append(""); } - private static void AppendChildren(StringBuilder sb, BreadcrumbRow row) + private static void AppendChildren(StringBuilder sb, BreadcrumbRow row, bool isSelected) { sb.Append("
"); - if (row.IsLeafExpanded) + if (row.IsLeafExpanded && row.DescendantNodes.Count > 0) + { + // #1002: keyed children render recursively; the selected descendant is marked + // only on the selected row. + AppendDescendants( + sb, + row.DescendantNodes, + 0, + isSelected ? row.SelectedDescendantId : null + ); + } + else if (row.IsLeafExpanded) { for (int i = 0; i < row.LeafChildren.Count; i++) { @@ -230,5 +245,54 @@ private static void AppendChildren(StringBuilder sb, BreadcrumbRow row) sb.Append("
"); } + + // Each node keeps the pinned first-level prefix, then its identity and depth. The node + // affordance follows the name, and an expanded node's children container is a sibling + // after the node element so closest('.child') resolves to the clicked node. + private static void AppendDescendants( + StringBuilder sb, + IReadOnlyList nodes, + int depth, + string? selectedId + ) + { + for (int i = 0; i < nodes.Count; i++) + { + BreadcrumbDescendantNode node = nodes[i]; + sb.Append("
") + .Append(WebUtility.HtmlEncode(node.Segment.DisplayName)) + .Append(""); + if (node.Segment.HasChildren) + { + sb.Append( + "−" : "false\">+") + .Append(""); + } + + sb.Append("
"); + if (node.Expanded) + { + sb.Append("
"); + AppendDescendants(sb, node.Children, depth + 1, selectedId); + sb.Append("
"); + } + } + } } } diff --git a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs index 4e2f6282c..bd10fd57d 100644 --- a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs +++ b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs @@ -62,7 +62,9 @@ public string SerializeOutbound(BreadcrumbOutboundMessage message) /// Deserializes an inbound bridge payload, validating the discriminator and the /// per-type required fields (segmentDoubleClick and segmentActivate require /// segmentIndex; renderedChildActivate requires childIndex; - /// arrowKey requires key; every type requires rowId). + /// arrowKey requires key; descendantToggle and + /// descendantActivate require a non-empty string nodeId, exposed as + /// ; every type requires rowId). /// /// The raw JSON payload from the hosted document. /// The validated inbound message. @@ -126,7 +128,20 @@ public BreadcrumbInboundMessage DeserializeInbound(string json) throw Fail("arrowKey requires a non-empty 'key' field."); } - return new BreadcrumbInboundMessage(type, rowId, segmentIndex, childIndex, key); + string? nodeId = null; + if ( + type == BreadcrumbMessageTypes.DescendantToggle + || type == BreadcrumbMessageTypes.DescendantActivate + ) + { + nodeId = OptionalString(root, "nodeId"); + if (string.IsNullOrEmpty(nodeId)) + { + throw Fail($"{type} requires a non-empty string 'nodeId' field."); + } + } + + return new BreadcrumbInboundMessage(type, rowId, segmentIndex, childIndex, key, nodeId); } private static bool IsKnownInboundType(string type) @@ -137,7 +152,9 @@ private static bool IsKnownInboundType(string type) || type == BreadcrumbMessageTypes.LeafExpandToggle || type == BreadcrumbMessageTypes.ArrowKey || type == BreadcrumbMessageTypes.RowSelected - || type == BreadcrumbMessageTypes.RowConfirmed; + || type == BreadcrumbMessageTypes.RowConfirmed + || type == BreadcrumbMessageTypes.DescendantToggle + || type == BreadcrumbMessageTypes.DescendantActivate; } private static string RequireString(JObject root, string fieldName) diff --git a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs index 3d34e9d4b..3e88db8dc 100644 --- a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs +++ b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs @@ -30,6 +30,18 @@ public static class BreadcrumbMessageTypes /// Inbound: confirm the currently selected filing destination. public const string RowConfirmed = "rowConfirmed"; + /// + /// Inbound: expand or collapse a keyed descendant node addressed by its host-issued node + /// identity (#1002). + /// + public const string DescendantToggle = "descendantToggle"; + + /// + /// Inbound: select a keyed descendant node addressed by its host-issued node identity as + /// the filing destination (#1002). + /// + public const string DescendantActivate = "descendantActivate"; + /// Outbound: render a full document or per-row fragment. public const string Render = "render"; @@ -42,8 +54,8 @@ public static class BreadcrumbMessageTypes /// /// Inbound bridge message (JS -> .NET), discriminated by : - /// { type, rowId, segmentIndex?, childIndex?, key? }. net48-safe plain class (no - /// record/init). + /// { type, rowId, segmentIndex?, childIndex?, key?, nodeId? }. net48-safe plain class + /// (no record/init). /// public sealed class BreadcrumbInboundMessage { @@ -55,13 +67,18 @@ public sealed class BreadcrumbInboundMessage /// Segment index for segment-scoped messages, or null. /// Child index for rendered-child activation messages, or null. /// Arrow-key name (Left/Right/Up/Down), or null. + /// + /// Host-issued descendant node identity for the descendant toggle and activate messages, + /// or null for every other type. Optional so existing call sites compile unchanged. + /// /// or is null. public BreadcrumbInboundMessage( string type, string rowId, int? segmentIndex, int? childIndex, - string? key + string? key, + string? nodeId = null ) { Type = type ?? throw new ArgumentNullException(nameof(type)); @@ -69,6 +86,7 @@ public BreadcrumbInboundMessage( SegmentIndex = segmentIndex; ChildIndex = childIndex; Key = key; + NodeId = nodeId; } /// Message discriminator. @@ -85,6 +103,12 @@ public BreadcrumbInboundMessage( /// Arrow-key name for ; null otherwise. public string? Key { get; } + + /// + /// Host-issued descendant node identity for + /// and ; null otherwise. + /// + public string? NodeId { get; } } /// diff --git a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs new file mode 100644 index 000000000..50f845eb0 --- /dev/null +++ b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs @@ -0,0 +1,200 @@ +#nullable enable +using System; +using System.Collections.Generic; + +namespace UtilitiesCS.OutlookObjects.Folder +{ + /// + /// Keyed descendant-tree members of (#1002). The first-level + /// children of the expanded active segment keep their provider keys as + /// instances, so deeper levels can be fetched and + /// selected by a host-issued node identity. Every transition validates that the identity + /// belongs to this row's current tree, and a fetch result is accepted only for the request + /// generation that started it. + /// + public sealed partial class BreadcrumbRow + { + private List _descendants = new List(); + + /// Identity of the selected descendant node, or null when none is selected. + public string? SelectedDescendantId { get; private set; } + + /// + /// First-level keyed descendant nodes of the expanded active segment; empty when the + /// children were stored key-less through . + /// + internal IReadOnlyList DescendantNodes => _descendants; + + /// + /// Stores keyed first-level children: the key-less projection used by the existing + /// first-level contract plus one descendant node per child. Valid only on a suggestion row + /// whose active segment has subfolders; otherwise a no-op. + /// + /// Immediate keyed children of the active segment. + /// True when the children were stored. + /// is null. + public bool SetLeafDescendants(IReadOnlyList children) + { + if (children == null) + { + throw new ArgumentNullException(nameof(children)); + } + + if (!CanExpandActiveSegment()) + { + return false; + } + + _leafChildren = new List(BreadcrumbRowBuilder.MapSegments(children)); + var nodes = new List(children.Count); + foreach (FolderBreadcrumbSegment child in children) + { + nodes.Add(new BreadcrumbDescendantNode(child)); + } + + _descendants = nodes; + SelectedDescendantId = null; + return true; + } + + /// + /// Starts expanding a known, collapsed descendant that has children: marks it expanded and + /// advances its request generation. + /// + /// Host-issued node identity. + /// The node's provider key when the expansion began; otherwise null. + /// The request generation to present when attaching. + /// True when the expansion began; false for an unknown, leaf, or expanded node. + public bool TryBeginDescendantExpand( + string nodeId, + out FolderTreeNodeKey? key, + out int generation + ) + { + key = null; + generation = 0; + BreadcrumbDescendantNode? node = FindDescendant(nodeId); + if (node == null || !node.Segment.HasChildren || node.Expanded) + { + return false; + } + + node.Expanded = true; + node.RequestGeneration++; + key = node.Segment.Key; + generation = node.RequestGeneration; + return true; + } + + /// Collapses an expanded descendant and its expanded subtree. + /// Host-issued node identity. + /// True when a known expanded node was collapsed. + public bool TryCollapseDescendant(string nodeId) + { + BreadcrumbDescendantNode? node = FindDescendant(nodeId); + if (node == null || !node.Expanded) + { + return false; + } + + node.Collapse(); + return true; + } + + /// + /// Attaches fetched children to a descendant only when the node is still in this row's + /// tree, still expanded, and its request generation still equals + /// . + /// + /// Host-issued node identity. + /// Generation returned by . + /// The fetched immediate children. + /// True when the children were attached; false for a stale completion. + public bool TryAttachDescendantChildren( + string nodeId, + int generation, + IReadOnlyList children + ) + { + BreadcrumbDescendantNode? node = FindDescendant(nodeId); + if (node == null || !node.Expanded || node.RequestGeneration != generation) + { + return false; + } + + node.Attach(children); + return true; + } + + /// + /// Abandons a failed or canceled expansion: collapses the node only when + /// is still its current request, so a later retry begins + /// from a collapsed node. + /// + /// Host-issued node identity. + /// Generation returned by . + /// True when the expansion was abandoned. + public bool TryAbandonDescendantExpand(string nodeId, int generation) + { + BreadcrumbDescendantNode? node = FindDescendant(nodeId); + if (node == null || node.RequestGeneration != generation) + { + return false; + } + + node.Collapse(); + return true; + } + + /// Returns the folder path of a descendant, or null for an unknown identity. + /// Host-issued node identity. + public string? GetDescendantPath(string nodeId) => + FindDescendant(nodeId)?.Segment.FolderPath; + + /// + /// Selects a known descendant, or clears the selection when is + /// null. An unknown identity leaves unchanged. + /// + /// Host-issued node identity, or null to clear. + /// True when the selection was set or cleared. + public bool TrySelectDescendant(string? nodeId) + { + if (nodeId == null) + { + SelectedDescendantId = null; + return true; + } + + if (FindDescendant(nodeId) == null) + { + return false; + } + + SelectedDescendantId = nodeId; + return true; + } + + // Called exactly where the first-level children stop being shown or are replaced. + private void ClearDescendants() + { + _descendants = new List(); + SelectedDescendantId = null; + } + + // An identity never issued by this row's current tree (including null or empty) finds + // nothing, because every issued identity is a non-empty GUID string. + private BreadcrumbDescendantNode? FindDescendant(string nodeId) + { + foreach (BreadcrumbDescendantNode node in _descendants) + { + BreadcrumbDescendantNode? found = node.Find(nodeId); + if (found != null) + { + return found; + } + } + + return null; + } + } +} diff --git a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs index 369b7dfbb..059f69bae 100644 --- a/UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs +++ b/UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs @@ -31,7 +31,7 @@ public enum BreadcrumbRowKind /// Right expands (restores a collapsed breadcrumb, else expands the leaf children), Left /// collapses (leaf children first, else hides the trailing segment). /// - public sealed class BreadcrumbRow + public sealed partial class BreadcrumbRow { private readonly List _segments; private List _leafChildren = new List(); @@ -167,6 +167,7 @@ public bool ActivateSegment(int segmentIndex) ActiveSegmentIndex = segmentIndex; _leafChildren = new List(); + ClearDescendants(); IsLeafExpanded = false; return true; } @@ -225,6 +226,7 @@ public bool CollapseAfter(int segmentIndex) CollapsedAfterIndex = segmentIndex; IsLeafExpanded = false; + ClearDescendants(); return true; } @@ -263,6 +265,7 @@ public bool SetLeafChildren(IEnumerable children) } _leafChildren = new List(children); + ClearDescendants(); // Key-less children carry no expandable descendants. return true; } @@ -279,6 +282,11 @@ public bool ToggleLeafExpanded() } IsLeafExpanded = !IsLeafExpanded; + if (!IsLeafExpanded) + { + ClearDescendants(); + } + return true; } @@ -298,6 +306,7 @@ public bool LeftArrow() if (IsLeafExpanded) { IsLeafExpanded = false; + ClearDescendants(); return true; } diff --git a/UtilitiesCS/UtilitiesCS.csproj b/UtilitiesCS/UtilitiesCS.csproj index 41cd460cb..a5d6cb9da 100644 --- a/UtilitiesCS/UtilitiesCS.csproj +++ b/UtilitiesCS/UtilitiesCS.csproj @@ -643,6 +643,7 @@ + diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T10-39.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T10-39.md new file mode 100644 index 000000000..8cc2bcbf3 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T10-39.md @@ -0,0 +1,41 @@ +# Code Review: EFC and QFC folder tree selection and nesting (Issue #1002) + +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002` +- Head: `e96e65302de00d3b0f8174736c90a59f82525dc9` +- Base: `main` at merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` +- Review timestamp: 2026-10-22T10-39 +- Scope: full branch diff (12 production C# files, 1 JavaScript asset, 8 C# test files, 1 node suite, 4 project files, feature documents) + +## Executive Summary + +The implementation follows the spec closely and is well structured. The EFC descendant tree is a small, cohesive partial on `BreadcrumbRow` with generation-checked transitions; the router partial applies the identity (I1), stale-completion (I2), toggle-versus-selection (I3) and archive-root (I4) invariants in the order the spec traces; the trash preselection is delivered inside the bound document (I5); and no `ConfigureAwait(false)` or lock was introduced in the UI-affine router (I6). The QFC change is confined to `makeSubfolders`. Tests are strict-mock, barrier-driven and readable. + +Blocking findings: 4 (CR-B1 to CR-B4; three autonomous, one requiring a maintainer decision). Non-blocking findings: 5. + +The one functional gap is CR-B4: the renderer marks the selected descendant with `aria-selected="true"`, but no stylesheet rule targets that attribute, and the row-level highlight (`.rowwrap.selected .row`) styles only the crumb line. After a user activates a grandchild, the containing row is highlighted with its own path while the actual filing destination has no visual mark. The manual check's step 3 ("confirm it is highlighted") cannot pass as built. + +## Findings Table + +| Severity | File | Location | Finding | Recommendation | Rationale | Evidence | +|---|---|---|---|---|---|---| +| Blocking | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | `LightThemeCss`, `DarkThemeCss`, `BaseCss` | CR-B4: the selected EFC descendant has no visual highlight; `aria-selected="true"` (emitted by `BreadcrumbHtmlRenderer.AppendDescendants`) is not targeted by any CSS rule, and `.rowwrap.selected .row` does not reach the `.children` container. | Add a theme-aware rule such as `.child[aria-selected="true"] { background: ; }` to both theme blocks (matching the row selection colours `#cce8ff` and `#264f78`), plus a C# text-contract test in a new or existing Issue1002 renderer test file. | The spec's expected behaviour and its manual validation step 3 require the selected subfolder to be visibly selected; the attribute-not-class choice in AC9 is compatible with an attribute selector. Remediability: autonomous. | Grep of `UtilitiesCS/OutlookObjects/Folder` for `aria-selected`: one hit, in the renderer only; `evidence/other/manual-outlook-check.2026-10-22T10-17.md` step 3. | +| Blocking | `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | `BindFolderRows`, `BindSourceFolderRows` | CR-B1: whole-file coverage lowered from 67.83% line / 57.14% branch to 64.46% / 52.38%; `BindFolderRows` lost its only tested caller when the delete gesture switched to `BindBreadcrumbRowsAsync`. | Add tests that reach `BindSourceFolderRows` and `BindFolderRows` (for example through `RefreshSuggestionsAsync` with a stubbed data model, or directly as internal members), covering the null-viewer, null-router and bind paths. | A change must not lower coverage, and the file is below the 80% / 75% modified-file floor. Remediability: autonomous. | Reviewer per-file union over `artifacts/csharp/coverage.xml` and the baseline Cobertura; `evidence/qa-gates/coverage-comparison.2026-10-22T10-26.md` observation. | +| Blocking | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | constructors at lines 84-85, 124, 144, 173-175 | CR-B2: branch coverage 50.00% (7/14), below the 75% floor; the uncovered arms are the `?? throw new ArgumentNullException` guards, including the `BreadcrumbInboundMessage` constructor modified by this diff. | Add null-argument tests for `type` and `rowId` on `BreadcrumbInboundMessage` and for the guarded parameters of the three outbound message constructors. | Modified files are held to the 75% branch floor. Remediability: autonomous. | Cobertura `condition-coverage` 50% (1/2) on each listed line, baseline and head. | +| Blocking | `QuickFiler/Controllers/EfcFormController.Actions.cs` and `EfcFormController.Breadcrumb.cs` | `ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `RefreshSuggestionsAsync`, `ConfigureBreadcrumbControl` | CR-B3: pre-existing residue keeps both files below the floor (Actions 23.42% / 33.33%; Breadcrumb at most 79.3% line even with CR-B1 fixed, because `ConfigureBreadcrumbControl` and its lambdas, 25 lines, construct the WebView2 host). | Maintainer decision: either ratify the CLAUDE.md UT2 COM/VSTO/WinForms exemption for these members (attribute or `coverage.config`), or authorize seam work (a message-box sink, a host factory) as a follow-up issue outside this bug's scope. | CLAUDE.md states the exemption "must be ratified by the project maintainer"; the residue is unchanged code in `MessageBox.Show`- and WebView2-bound members. Remediability: human_decision_required. | Per-method Cobertura: `ActionOkAsync` 0/28, `CreateFolderAsync` 0/41, `ConfigureBreadcrumbControl` 0/22. | +| Minor | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | `BaseCss` `.child` rule | CR-1: `.child` is now a flex container, so `text-overflow: ellipsis` on it no longer truncates the name text (ellipsis applies to inline content, not to flex items); long names are clipped without an ellipsis. | Move `min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;` onto `.child .name`. | Preserves the pre-change truncation behaviour for long folder names. | Diff of `BaseCss`. | +| Minor | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | `AppendDescendants` | CR-2: a node is marked `Expanded` before its fetch completes; any row render during the fetch shows the minus affordance over an empty `.children` container. | Acceptable for now; optionally render the affordance as `+` until children are attached, or note it in the manual check. | Transient visual state only; no data impact. | `TryBeginDescendantExpand` sets `Expanded = true` before the await. | +| Minor | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | `AppendDescendants` | CR-3: nested keyed nodes also carry `data-child-index` set to their index within their own level. The keyed branch precedes the key-less branch in `BridgeJs`, so no wrong message is posted today. | Keep the AC10 ordering test; consider emitting `data-child-index` only at depth zero (the pinned prefix applies to first-level children). | A future reorder of the click branches would post `renderedChildActivate` with an index that does not address a first-level child. | `BridgeJs` click listener order; AC10 tests. | +| Minor | `QuickFiler/Controllers/EfcFormController.Actions.cs` | `ApplyDeleteGestureAsync`, line 102 | CR-4: the `_formViewer == null` arm is unreached (3 of 4 conditions). | Add one direct test with a null viewer, or leave it; the method meets the 75% branch threshold. | Completeness of the guard's tests. | Cobertura `condition-coverage` 75% (3/4) on line 102. | +| Info | `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | `HandleDescendantToggleAsync` | CR-5: expected races (superseded fetch, collapse during fetch) are logged at `Error`, as the spec requires. | Consider `Warn` or `Debug` for superseded completions in a later change. | Error-level noise for a normal user interaction. | Spec "Error handling and logging updates". | + +## Positive Observations + +- `TryGetRowSelection` removes the duplication the spec warned about; `SelectRow` and `SelectPreferredRow` share one normalization path. +- `SelectHierarchyPath` applies the descendant highlight only after the archive-root checks, so a rejected path leaves the prior highlight unchanged (I4). +- The first-level `ExpandLeafAsync` gained the same-instance guard (D4) with a comment explaining positional row-id reuse. +- Stale-completion tests use `TaskCompletionSource` barriers and `SetupSequence`, with no timing dependence. +- Node identities and folder paths are HTML-encoded in the renderer; the bridge reads identities with `getAttribute` only. + +## Verdict + +**REMEDIATION_REQUIRED.** CR-B1, CR-B2 and CR-B4 are autonomous fixes inside files already in the branch diff; CR-B3 needs a maintainer decision. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T13-37.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T13-37.md new file mode 100644 index 000000000..bad02a028 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T13-37.md @@ -0,0 +1,51 @@ +# Code Review: EFC and QFC folder tree selection and nesting (Issue #1002) + +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002` +- Head: `b72ac1dbbfdb6b45d01a14dde9e91428a6bf41ac` +- Base: `main` at merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` +- Review timestamp: 2026-10-22T13-37 (re-audit after remediation cycle 1) +- Scope: full branch diff (12 production C# files, 1 JavaScript asset, 9 C# test files, 1 node suite, 4 project files, feature documents and evidence); the cycle-1 commit `b72ac1dbb` was read in full against `e96e65302`. + +## Executive Summary + +Cycle 1 resolves all four cycle-0 blocking findings with small, targeted changes. The CSS fix uses an attribute selector, preserving the AC9 decision to mark the selected descendant by `aria-selected` rather than a class; because the renderer emits an expanded node's `.children` container as a sibling after the node element, the highlight covers only the selected node's line. The `.child .name` rule restores ellipsis truncation for keyed descendants. The coverage exemption is applied at member level to exactly the five members the maintainer named, with an explanatory comment on each. The new tests are focused and readable. + +Blocking findings: 1 (R-1, autonomous). Non-blocking findings: 5 new (N-1 to N-5) plus three carried from cycle 0 (CR-2, CR-3, CR-5). + +R-1 is a requirements-traceability defect, not a code defect: spec AC23 still states "no coverage exclusion is added" and is checked, while the branch now adds five ratified `[ExcludeFromCodeCoverage]` attributes. + +## Findings Table + +| Severity | File | Location | Finding | Recommendation | Rationale | Evidence | +|---|---|---|---|---|---|---| +| Blocking | `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md` | `## Acceptance Criteria`, AC23 | R-1: AC23 states "no coverage exclusion is added" and is checked `[x]`, but the branch adds five member-level `[ExcludeFromCodeCoverage]` attributes. The maintainer ratification (B-3 decision, 2026-10-22T10:50Z) lifted the plan's prohibition but was not carried into the spec, so the merged criterion is false as written. | Amend AC23 to state the ratified exception (for example, "no coverage exclusion is added other than the five member-level `[ExcludeFromCodeCoverage]` attributes ratified by the maintainer under CLAUDE.md UT2, recorded in remediation-inputs.2026-10-22T10-39.md") and add a dated planner-amendment note in `spec.md` citing the decision; keep the box checked only after the amendment. | The AC record must agree with the delivered code; the substantive decision already exists, so the change is clerical. Remediability: autonomous. | `spec.md` line 298; `git diff 8e4ad1ecb...HEAD -- '*.cs'` shows 5 added `[ExcludeFromCodeCoverage]` lines; `remediation-inputs.2026-10-22T10-39.md` section "B-3 Maintainer decision". | +| Minor | `QuickFiler/Controllers/EfcFormController.Actions.cs` | comment above `ActionCancelAsync` | N-1: the exemption comment says the handler "closes the live WinForms viewer, whose MessageBox-bound form cannot run headless"; `ActionCancelAsync` calls no `MessageBox`. Its actual dependency is the concrete `EfcViewer` form (`UiSyncContext` awaiter and `Close`). | Reword to name the concrete `EfcViewer` dependency. | Exemption justifications are the reviewable record of why a member is outside the metric; they should be accurate. | `_formViewer` is declared `private EfcViewer _formViewer;` in `EfcFormController.cs`; method body lines 73-80. | +| Minor | `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | three async bind tests (lines 94, 117, 173) | N-3: each test awaits `delivered.Task`, completed only by the host mock's `NavigateToString` callback. `BindFolderRows` discards the bind task, and `BindBreadcrumbRowsAsync` catches every exception, so a future regression that faults before delivery leaves the test waiting indefinitely rather than failing. | Complete the `TaskCompletionSource` from the fault path as well (for example, fail it from a `TryReportBoundaryFault` seam), or assert through `BindBreadcrumbRowsAsync` directly where the bind must be observed. Do not add a wall-clock timeout. | A hung test is harder to diagnose than a failed one; the current behaviour is correct, so this is a robustness item. | Test methods starting at lines 94, 117 and 173; `BindFolderRows` line 175 `_ = BindBreadcrumbRowsAsync(...)`. | +| Minor | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs`, `BreadcrumbHtmlRenderer.cs` | `BaseCss` `.child`; bridge `subfolderResult` handler; `AppendChildren` key-less branch | N-5: after CR-1, `.child` no longer carries `white-space: nowrap; overflow: hidden; text-overflow: ellipsis`, so `.child` elements whose text is bare (no `.name` span) wrap instead of truncating. Two emitters produce bare text: the bridge `subfolderResult` handler (transient; replaced by the row render that follows) and the renderer's key-less branch (reached only through `SetLeafChildren`, which no production caller uses; the router calls `SetLeafDescendants`). | Optional: wrap the text in `` in both emitters, or leave as is. | Low user impact: one path is transient and the other is not reached in production. | `evidence/other/child-name-markup-check.2026-10-22T13-28.md`; Grep: the only production caller is `BreadcrumbBridgeRouter.Arrows.cs:201` `SetLeafDescendants`. | +| Info | `QuickFiler/Controllers/EfcFormController.Actions.cs`, `.Breadcrumb.cs` | five attributed members | N-2: the decision asked for a `Justification` on each attribute; the property does not exist on the .NET Framework 4.8.1 attribute, so a comment is used. | None. | Verified by a compile probe (CS0246). | `evidence/other/exclude-attribute-form.2026-10-22T13-25.md`. | +| Info | `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | `InvokeIssue1002Bind` | N-4: `BindFolderRows` and `BindSourceFolderRows` are private and invoked by name through reflection; a rename fails the test at the `member.Should().NotBeNull(...)` guard rather than at compile time. | Optional: make the two members `internal` (the test project already reaches internal members) and call them directly. | Consistent with the file's existing `SetPrivateField` pattern; the guard message makes a rename failure clear. | Lines 246-258. | +| Minor | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | `AppendDescendants` | CR-2 (carried): a node shows the minus affordance over an empty container while its fetch is in flight. | No change required in this item. | Transient visual state only. | Cycle-0 code review. | +| Minor | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | `AppendDescendants` | CR-3 (carried): nested keyed nodes carry `data-child-index` relative to their own level; safe under the current click-branch order. | Keep the AC10 ordering test. | Future reorder risk only. | Cycle-0 code review. | +| Info | `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | `HandleDescendantToggleAsync` | CR-5 (carried): expected races are logged at `Error`, as the spec requires. | Consider a lower level in a later change. | Log noise only. | Cycle-0 code review. | + +## Cycle-0 Finding Resolution + +| Cycle-0 finding | Status | Evidence | +|---|---|---| +| CR-B1 `EfcFormController.Breadcrumb.cs` coverage lowered | Resolved | 8 new tests; file 93/101 lines (92.08%), 34/42 branches (80.95%) against required 84 lines and 24 branches (reviewer recomputation). | +| CR-B2 `BreadcrumbMessages.cs` branch coverage | Resolved | 7 null-guard tests, one per guarded argument, each asserting `ParamName`; 14/14 branches. The `NullTypeOutboundMessage` test subclass is the only route to the protected base constructor and is commented as such. | +| CR-B3 pre-existing residue | Resolved by maintainer-ratified exemption | Attributes on exactly the five named members; no class-level attribute; no `coverage.config` change; `RefreshSuggestionsAsync` admitted only after evidence it stayed at 0 of 9 lines. | +| CR-B4 no descendant highlight | Resolved | `.child[aria-selected="true"]` rules in both theme blocks with the row selection colours; text-contract tests assert both rules beside the row rule. | +| CR-1 ellipsis on flex container | Resolved for keyed descendants | `.child .name` carries the truncation properties; `BaseCss_ChildName_CarriesEllipsisRule` also asserts `.child` no longer carries `text-overflow` and keeps `display: flex`. See N-5 for bare-text emitters. | +| CR-4 null-viewer arm | Resolved | `ApplyDeleteGestureAsync_NullViewer_RetainsTrashRowAndDoesNotBind`; covered branches 12 to 13. | + +## Positive Observations + +- The production change in cycle 1 is limited to CSS constants, attributes, comments and `using` directives; no behaviour of the delivered fix changed. +- Each null-guard test asserts the exception type and the parameter name, so a guard that throws for the wrong argument fails. +- The CSS contract tests pin the selected-descendant colour beside the row colour in the same assertion chain, which keeps the two in step. +- The RED-first projection for the CSS work fails exactly the three new tests and passes the ten existing ones. + +## Verdict + +**REMEDIATION_REQUIRED.** All code-level findings from cycle 0 are resolved. R-1 is an autonomous spec amendment that aligns AC23 with the ratified exemption; N-1, N-3 and N-5 are optional in-scope improvements. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T14-10.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T14-10.md new file mode 100644 index 000000000..70867ae73 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T14-10.md @@ -0,0 +1,45 @@ +# Code Review: EFC and QFC folder tree selection and nesting (Issue #1002) + +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002` +- Head: `54b36112abf0a4ac26384226694b02520842061b` +- Base: `main` at merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` +- Review timestamp: 2026-10-22T14-10 (re-audit after remediation cycle 2) +- Scope: full branch diff (12 production C# files, 1 JavaScript asset, 9 C# test files, 1 node suite, 4 project files, feature documents and evidence); the cycle-2 commits were read in full against `9ccf91971`. + +## Executive Summary + +Cycle 2 resolves the single cycle-1 blocking finding R-1 and the three recommended in-scope items. The AC23 amendment changes only the named clause and records its authority in a dated `## Planner Amendments` entry. The fault exit for the delivered waits is wired once in the shared test helper, so every caller gains it, and the new proof test observes the fault synchronously with no timer. The `subfolderResult` handler now places the display name in a `span.name`, which brings the transient render under the cycle-1 truncation rule; because every bridge click branch resolves its target with `closest(...)`, wrapping the text does not change click routing. The renderer's key-less branch was left unedited under the recorded AC9 ruling, which is consistent with AC9's "key-less children render exactly as before" clause. + +Blocking findings: 0. Non-blocking findings: 2 new (N-6, N-7) plus five carried (N-2, N-4, CR-2, CR-3, CR-5). + +## Findings Table + +| Severity | File | Location | Finding | Recommendation | Rationale | Evidence | +|---|---|---|---|---|---|---| +| Info | `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | `CreateIssue1002Controller`, `BoundaryErrorSink` assignment | N-6: the sink calls `delivered?.TrySetException(exception)`; if a future caller reported a fault with a null exception, `TrySetException` would throw `ArgumentNullException` inside the sink. The production `TryReportBoundaryFault` wrapper invokes the sink defensively, and every current report passes a non-null exception, so no test is affected today. | Optional: guard with `exception ?? new InvalidOperationException(message)`. | Robustness of a test helper only. | `EfcFormController.cs` `BoundaryErrorSink` and its defensive invoker `TryReportBoundaryFault`; cycle-2 diff of the helper. | +| Info | `QuickFiler/Controllers/EfcFormController.Actions.cs` | comment above `ActionCancelAsync` | N-7: the reworded exemption comment is a single 140-character line, longer than the sibling comments. CSharpier does not wrap comments and the format check passes. | Optional: wrap to match the neighbouring comment width. | Readability only. | Cycle-2 diff; `cycle2-format-check.2026-10-22T14-06.md`. | +| Info | `QuickFiler/Controllers/EfcFormController.Actions.cs`, `.Breadcrumb.cs` | five attributed members | N-2 (carried): net48 `ExcludeFromCodeCoverage` has no `Justification` property, so a comment is used. | None. | Verified by a compile probe (CS0246). | `evidence/other/exclude-attribute-form.2026-10-22T13-25.md`. | +| Info | `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | `InvokeIssue1002Bind` | N-4 (carried): `BindFolderRows` and `BindSourceFolderRows` are invoked by name through reflection; a rename fails at the `member.Should().NotBeNull(...)` guard rather than at compile time. | Optional: make the two members `internal` and call them directly. | The guard message makes a rename failure clear. | `InvokeIssue1002Bind` helper. | +| Minor | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | `AppendDescendants` | CR-2 (carried): a node shows the minus affordance over an empty container while its fetch is in flight. | No change required in this item. | Transient visual state only. | Cycle-0 code review. | +| Minor | `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | `AppendDescendants` | CR-3 (carried): nested keyed nodes carry `data-child-index` relative to their own level; safe under the current click-branch order. | Keep the AC10 ordering test. | Future reorder risk only. | Cycle-0 code review. | +| Info | `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | `HandleDescendantToggleAsync` | CR-5 (carried): expected races are logged at `Error`, as the spec requires. | Consider a lower level in a later change. | Log noise only. | Cycle-0 code review. | + +## Cycle-1 Finding Resolution + +| Cycle-1 finding | Status | Evidence | +|---|---|---| +| R-1 AC23 text contradicts the ratified exemption | Resolved | `spec.md` AC23 now reads "no coverage exclusion is added other than the five member-level `[ExcludeFromCodeCoverage]` attributes on `ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `RefreshSuggestionsAsync` and `ConfigureBreadcrumbControl` ratified by the maintainer ...; no `coverage.config` entry and no class-level exclusion is added"; the spec diff in cycle 2 is exactly that clause plus the `## Planner Amendments` section; reviewer count of added attributes is 5; `coverage.config` unchanged. | +| N-1 inaccurate exemption comment | Resolved | Comment names the concrete `EfcViewer` `UiSyncContext` awaiter and `Close`; attribute and body unchanged. | +| N-3 delivered waits had no failure exit | Resolved | `CreateIssue1002Controller` sets `BoundaryErrorSink` to complete the delivered source with the fault; `BindFolderRows_FaultBeforeDelivery_FaultsDeliveredTaskPromptly` asserts `IsFaulted` synchronously; fail-before projection shows an assertion failure, not a hang. `ActionDeleteAsync_BindFault_IsReportedAtBindBoundary` installs its own sink after creation and is unaffected. | +| N-5 bare `.child` text | Resolved as narrowed | `subfolderResult` handler wraps the display name in `span.name`; `BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan` pins the three statements and the absence of `div.textContent`. Renderer key-less branch untouched per the AC9 ruling. | + +## Positive Observations + +- The fault exit is placed in the shared helper, so the three existing waits gain it without edits to their bodies. +- The new fault test exploits the synchronous fault path (`_globals` unset faults before the first await), which removes any need for a timeout or wall-clock wait. +- The `subfolderResult` text-contract test anchors on the branch marker with `StringComparison.Ordinal` and asserts the negative (`div.textContent` absent), so a regression to bare text fails. +- The scope-fence and R-1 verification evidence counts the attributes and the `coverage.config` stat on the final tree, not only before the edit. + +## Verdict + +**PASS.** All blocking findings from cycles 0 and 1 are resolved, and the cycle-2 changes introduce no blocking defect. The remaining items are optional robustness and readability notes. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-coverage.2026-10-22T09-54.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-coverage.2026-10-22T09-54.md new file mode 100644 index 000000000..0061e3015 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-coverage.2026-10-22T09-54.md @@ -0,0 +1,28 @@ +# Baseline: TEST-COVERAGE (#1002) + +Timestamp: 2026-10-22T09-54 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +EXIT_CODE: 0 +Output Summary: +- Test run outcome Completed: total 7504, executed 7504, passed 7504, failed 0 (summary derived from the trx by the coverage script; skipped 0 derived as total minus executed). +- Shell-icon carve-out: not needed; the run did not stall. +- First-party coverage summary line: `First-party coverage: lines 57272/66515 (86.10%), branches 13923/17337 (80.31%)`. +- Repository line coverage 86.10 percent (floor 80); branch coverage 80.31 percent (floor 75). Post-processed Cobertura root: line-rate 0.861039, branch-rate 0.80308. +- Post-processed Cobertura copied to SCRATCH as `issue-1002-baseline-cobertura.xml` (not committed); trx copied to SCRATCH as `issue-1002-baseline.trx`. + +## Per-file line coverage (existing Write Set production files) + +Computed from the post-processed Cobertura by unioning `` elements of every `` whose filename is the file (a line counts as covered when any class entry reports hits above zero). Helper: SCRATCH/issue-1002-filecov.ps1. + +| File | Covered/Valid | Line coverage | +|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | 153/155 | 98.71% | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | 30/30 | 100.00% | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | 107/107 | 100.00% | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | 129/133 | 96.99% | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | 0/0 | not instrumented (string constants only; no executable lines) | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 239/241 | 99.17% | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | 128/133 | 96.24% | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | 150/153 | 98.04% | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | 23/108 | 21.30% | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | 78/115 | 67.83% | diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-file-sizes.2026-10-22T09-52.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-file-sizes.2026-10-22T09-52.md new file mode 100644 index 000000000..785eb8ea0 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-file-sizes.2026-10-22T09-52.md @@ -0,0 +1,38 @@ +# Baseline: Physical Line Counts (#1002) + +Timestamp: 2026-10-22T09-52 +Command: pwsh: foreach file, @(Get-Content -LiteralPath ).Count (launcher SCRATCH/issue-1002-linecount.ps1) +EXIT_CODE: 0 +Output Summary: physical line counts for every existing Write Set code file and the seven near-limit test files; new Write Set files are absent at baseline. + +## Write Set code files (existing) + +| File | Lines | +|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | 361 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | 173 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | 203 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | 234 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | 149 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 459 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | 211 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | 236 | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | 184 | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | 208 | +| `QuickFiler/Resources/FolderBreadcrumb.js` | 347 | + +## Write Set code files (new; absent at baseline) + +`BreadcrumbRow.Descendants.cs`, `BreadcrumbBridgeRouter.Descendants.cs`, and the eight new test files (`BreadcrumbMessageCodecIssue1002Tests.cs`, `BreadcrumbRowDescendantsIssue1002Tests.cs`, `BreadcrumbHtmlRendererIssue1002Tests.cs`, `BreadcrumbBridgeRouterIssue1002Tests.cs`, `BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs`, `BreadcrumbBridgeRouterTrashIssue1002Tests.cs`, `EfcFormControllerTests.Issue1002.cs`, `FolderBreadcrumbNestingIssue1002Tests.cs`) and `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs`: 0 (absent). + +## Near-limit test files (must not be extended) + +| File | Lines | +|---|---| +| QuickFiler.Test/Controllers/EfcFormControllerTests.cs | 485 | +| QuickFiler.Test/Controllers/EfcFormControllerTests.Part2.cs | 490 | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTests.cs | 462 | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterQueueTests.cs | 462 | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterScoreJoinTests.cs | 463 | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue439Tests.cs | 455 | +| QuickFiler.Test/Viewers/FolderBreadcrumbAssetContractTests.cs | 415 | diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-node-suites.2026-10-22T09-53.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-node-suites.2026-10-22T09-53.md new file mode 100644 index 000000000..e1d97f337 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-node-suites.2026-10-22T09-53.md @@ -0,0 +1,10 @@ +# Baseline: Node Suites over FolderBreadcrumb.js (#1002) + +Timestamp: 2026-10-22T09-53 +Command: node --test --test-isolation=none --test-concurrency=1 --experimental-test-coverage --test-coverage-include=QuickFiler\Resources\FolderBreadcrumb.js --test-reporter=spec tests/quickfiler-breadcrumb/issue-997-common.test.mjs tests/quickfiler-breadcrumb/issue-997-recursive.test.mjs +EXIT_CODE: 0 +Output Summary: +- Node v24.14.0. +- tests 78, pass 78, fail 0, cancelled 0, skipped 0. +- `FolderBreadcrumb.js` coverage: line 100.00 percent, branch 100.00 percent, funcs 100.00 percent. +- Raw log kept under SCRATCH as `issue-1002-baseline-node.log`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-test-disposition.2026-10-22T09-53.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-test-disposition.2026-10-22T09-53.md new file mode 100644 index 000000000..e6db916d6 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-test-disposition.2026-10-22T09-53.md @@ -0,0 +1,15 @@ +# Baseline: Test Disposition for Changed Members (#1002) + +Timestamp: 2026-10-22T09-53 +Command: Grep (ripgrep) over `UtilitiesCS.Test/`, `QuickFiler.Test/`, `tests/quickfiler-breadcrumb/` for `(GetField|GetMethod|GetProperty)\(.*(ApplyDeleteGesture|ActionDeleteAsync|SelectHierarchyPath|SelectRow|ExpandLeafAsync|BindRowsAsync|BindBreadcrumbRowsAsync|SetLeafChildren|AppendChildren|IsKnownInboundType|makeSubfolders)`; second pass for `ApplyDeleteGesture`, `ButtonDeleteClickAsync`, `nameof()`, and quoted member-name string literals. +EXIT_CODE: 0 +Output Summary: +- Same-line reflection pattern: 0 matches. +- Name-string pass: 2 matches, both in `QuickFiler.Test/Controllers/EfcFormControllerTests.cs`; both unchanged. + +| Test / location | Match | Disposition | +|---|---|---| +| `AsyncVoidBoundary_WhenFaulted_LogsOnceAndDoesNotThrow` (`[DataRow("ButtonDeleteClickAsync")]`, resolved through `GetMethod(memberName, ...)`) | reaches `ButtonDeleteClickAsync` by name, which calls `ActionDeleteAsync` | Unchanged: `ActionDeleteAsync` keeps awaiting the viewer UI context as its first statement, so the null `_formViewer` still faults first and the boundary still logs once. | +| Comment above `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows` | comment-only `ApplyDeleteGesture` reference | Unchanged: comment text, not a reflection lookup; the test itself relies on the null-router guard, which P5-T2 preserves. | + +No further match; no test must change, so the plan proceeds without a planner revision. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-analyzers.2026-10-22T09-51.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-analyzers.2026-10-22T09-51.md new file mode 100644 index 000000000..f3db7ff3e --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-analyzers.2026-10-22T09-51.md @@ -0,0 +1,10 @@ +# Baseline: ANALYZE (#1002) + +Timestamp: 2026-10-22T09-51 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: +- `0 Warning(s)`, `0 Error(s)`; Time Elapsed 00:00:13.60. +- CoreCompile ran (the log records `QuickFiler -> ...QuickFiler.dll` and `UtilitiesCS.Test -> ...UtilitiesCS.Test.dll` after the Rebuild clean step). +- Warnings whose path is a Write Set file: none. +- MSBuild resolved through vswhere; raw log kept under SCRATCH as `issue-1002-baseline-analyze.log`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-bootstrap.2026-10-22T09-50.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-bootstrap.2026-10-22T09-50.md new file mode 100644 index 000000000..8847bc6ed --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-bootstrap.2026-10-22T09-50.md @@ -0,0 +1,13 @@ +# Baseline: Toolchain Bootstrap (#1002) + +Timestamp: 2026-10-22T09-50 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Install-RepoDotNetSdk.ps1; dotnet tool restore; pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-Restore.ps1 -SolutionPath TaskMaster.sln -Configuration Debug -Platform "Any CPU"; git status --porcelain +EXIT_CODE: 0 +Output Summary: +- Install-RepoDotNetSdk.ps1: EXIT_CODE 0; installed repo-local .NET SDK 8.0.205. +- dotnet tool restore: EXIT_CODE 0; csharpier 1.2.6 restored. +- Invoke-Restore.ps1: EXIT_CODE 0; 173 packages installed to packages.config projects; 0 Warning(s), 0 Error(s). +- dotnet-coverage global tool present. +- git status --porcelain after the restore lists only feature-folder paths (the plan file check-off and `evidence/baseline/`); no tracked source or project file changed. + +Launcher script and raw log are kept under SCRATCH (`issue-1002-bootstrap.ps1`, `issue-1002-bootstrap.log`) and are not committed. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-format.2026-10-22T09-51.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-format.2026-10-22T09-51.md new file mode 100644 index 000000000..a363d191b --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-format.2026-10-22T09-51.md @@ -0,0 +1,8 @@ +# Baseline: FORMAT (read-only check) (#1002) + +Timestamp: 2026-10-22T09-51 +Command: dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: +- `Checked 1664 files in 5208ms.` +- No drifting files; no `format` was run, so nothing was repaired. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-typecheck.2026-10-22T09-51.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-typecheck.2026-10-22T09-51.md new file mode 100644 index 000000000..99eebc6f9 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain-typecheck.2026-10-22T09-51.md @@ -0,0 +1,8 @@ +# Baseline: TYPECHECK (#1002) + +Timestamp: 2026-10-22T09-51 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +EXIT_CODE: 0 +Output Summary: +- `0 Warning(s)`, `0 Error(s)`; Time Elapsed 00:00:11.82. +- No `/p:Nullable=enable` (per CLAUDE.md C#1.3). Raw log kept under SCRATCH as `issue-1002-baseline-typecheck.log`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/cycle2-phase0-instructions-read.2026-10-22T13-51.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/cycle2-phase0-instructions-read.2026-10-22T13-51.md new file mode 100644 index 000000000..8db28c300 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/cycle2-phase0-instructions-read.2026-10-22T13-51.md @@ -0,0 +1,14 @@ +# Cycle 2 Phase 0: instructions read (#1002, R3) + +Timestamp: 2026-10-22T13-51 +Policy Order: CLAUDE.md; .claude/rules/general-code-change.md; .claude/rules/general-unit-test.md; .claude/rules/csharp.md +Files read: +- CLAUDE.md +- .claude/rules/general-code-change.md +- .claude/rules/general-unit-test.md +- .claude/rules/csharp.md +- docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-inputs.2026-10-22T13-37.md +- docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T13-37.md +- docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T13-37.md +- docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T13-37.md +- docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T13-43.md diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/phase0-instructions-read.2026-10-22T09-49.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/phase0-instructions-read.2026-10-22T09-49.md new file mode 100644 index 000000000..7f69f6981 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/phase0-instructions-read.2026-10-22T09-49.md @@ -0,0 +1,23 @@ +# Phase 0 Instructions Read (#1002) + +Timestamp: 2026-10-22T09-49 +Branch: bug/efc-qfc-folder-tree-selection-and-nesting-1002 +Policy Order: CLAUDE.md -> .claude/rules/general-code-change.md -> .claude/rules/general-unit-test.md -> .claude/rules/csharp.md (policy-compliance-order), then supporting rules and skills. + +Files read, in order: + +1. `CLAUDE.md` +2. `.claude/rules/general-code-change.md` +3. `.claude/rules/general-unit-test.md` +4. `.claude/rules/csharp.md` +5. `.claude/rules/plan-acceptance-gates.md` +6. `.claude/skills/execution-environment/SKILL.md` +7. `.claude/skills/evidence-and-timestamp-conventions/SKILL.md` +8. `.claude/skills/acceptance-criteria-tracking/SKILL.md` +9. `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md` +10. `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/research/2026-10-22T09-17-efc-qfc-folder-tree-selection-and-nesting-research.md` + +Notes: + +- Work mode full-bug; acceptance-criteria source is `spec.md` only (AC1 to AC26). +- CLAUDE.md coverage floors (80 percent line, 75 percent branch; 90 percent for new code) govern over the rule-file defaults. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/remediation-phase0-instructions-read.2026-10-22T13-19.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/remediation-phase0-instructions-read.2026-10-22T13-19.md new file mode 100644 index 000000000..5ef6890c0 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/remediation-phase0-instructions-read.2026-10-22T13-19.md @@ -0,0 +1,11 @@ +# Remediation Phase 0: instructions read (#1002, cycle 1) + +Timestamp: 2026-10-22T13-19 +Policy Order: CLAUDE.md; .claude/rules/general-code-change.md; .claude/rules/general-unit-test.md; .claude/rules/csharp.md +Files read: +- CLAUDE.md +- .claude/rules/general-code-change.md +- .claude/rules/general-unit-test.md +- .claude/rules/csharp.md +- docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-inputs.2026-10-22T10-39.md (including the B-3 maintainer decision) +- docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T10-45.md diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/child-name-markup-check.2026-10-22T13-28.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/child-name-markup-check.2026-10-22T13-28.md new file mode 100644 index 000000000..9db0420a4 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/child-name-markup-check.2026-10-22T13-28.md @@ -0,0 +1,11 @@ +# `.child` emitter markup check (P3-T7, #1002 cycle 1) + +Timestamp: 2026-10-22T13-28 +Command: Grep over UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs, UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs and QuickFiler/Resources/FolderBreadcrumb.js for `child`, `name`, `makeSubfolders` and `className = 'child'` +EXIT_CODE: 0 +Output Summary: +- BreadcrumbHtmlRenderer.AppendDescendants (keyed descendants, first and deeper levels): emits `
` with the name wrapped as ``. The new `.child .name` rule applies. +- BreadcrumbHtmlRenderer.AppendChildren, `row.IsLeafExpanded` branch (keyless leaf children): emits `
` with bare HTML-encoded text and no `.name` span. Observation: this emitter keeps `display: flex` from `.child`, but its text is an anonymous flex item that no `.child .name` rule truncates. Not edited (BreadcrumbHtmlRenderer.cs is outside this plan's file set). +- BreadcrumbDocumentAssets bridge JS `subfolderResult` handler: creates `div.className = 'child'` with `textContent` set directly, bare text and no `.name` span. Observation only; the JS constant is outside the CSS constants this task edits. +- QuickFiler/Resources/FolderBreadcrumb.js `makeSubfolders` (QFC selector): builds its own subfolder elements with a `seg` span; it is styled by the QFC page stylesheet, not by BaseCss, so CR-1 does not affect it. +- manual-outlook-check step 3 is unchanged; the existing manual-check artifact is not edited. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/cycle2-child-name-dispositions.2026-10-22T14-02.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/cycle2-child-name-dispositions.2026-10-22T14-02.md new file mode 100644 index 000000000..4d20b4aeb --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/cycle2-child-name-dispositions.2026-10-22T14-02.md @@ -0,0 +1,23 @@ +# Cycle 2 P4-T5: N-5 test dispositions + +Timestamp: 2026-10-22T14-02 +Command: git grep -l -F -e -- '*Test*' for data-child-index, class="child", renderedChildActivate, subfolderResult, BridgeJs; git diff HEAD --stat -- UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs; git diff 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- the same path +EXIT_CODE: 0 +Output Summary: +Search hit lists (test files): +- data-child-index: BreadcrumbHtmlRendererIssue1002Tests.cs, BreadcrumbHtmlRendererTests.cs +- class="child": no hit as a fixed string in test files +- renderedChildActivate: BreadcrumbBridgeRouterIssue439Tests.Activation.cs, BreadcrumbBridgeRouterIssue439Tests.cs, BreadcrumbBridgeRouterIssue614Tests.cs, BreadcrumbBridgeRouterIssue995Tests.cs, BreadcrumbHtmlRendererIssue1002Tests.cs, BreadcrumbHtmlRendererTests.cs, BreadcrumbMessageCodecTests.cs +- subfolderResult: BreadcrumbBridgeRouterIssue1002Tests.cs, BreadcrumbBridgeRouterQueueTests.cs, BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs, BreadcrumbBridgeRouterTests.cs, BreadcrumbHtmlRendererIssue1002Tests.cs, BreadcrumbMessageCodecTests.cs +- BridgeJs: BreadcrumbHtmlRendererIssue1002Tests.cs, BreadcrumbHtmlRendererTests.cs + +Dispositions (all unchanged because unaffected; the scoped run of both renderer classes passes 29 of 29): +- BreadcrumbHtmlRendererIssue1002Tests.RenderRowFragment_KeylessChildren_RenderUnchanged: unchanged; the renderer is not edited, the key-less server render stays as before (AC9). +- BreadcrumbHtmlRendererTests.Issue439ActiveAncestorChildrenAndEmbeddedBridgeUseTypedStoppedActivation: unchanged; the renderer prefix and the renderedChildActivate branch are not touched. +- BreadcrumbHtmlRendererTests.RenderRowFragment_ExpandedLeaf_RendersEncodedChildren: unchanged; renderer not edited. +- Keyed-child, BridgeJs ordering, BaseCss and theme tests in BreadcrumbHtmlRendererIssue1002Tests.cs: unchanged; they index on .child[data-node-id], .child[data-child-index], type: 'renderedChildActivate' and CSS rules, none of which change. +- QuickFiler.Test router and codec tests (subfolderResult hits): unchanged; they assert the subfolderResult JSON message, not the page script. +- Node suites under QuickFiler.Test/Viewers: unchanged; they assert FolderBreadcrumb.js, not BridgeJs. +- The only test reading the subfolderResult handler text is the new BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan. + +Renderer unedited: git diff HEAD --stat for UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs is empty (no edit in this cycle). Note: the diff against the merge base is non-empty because earlier committed branch work (cycles 0 and 1) already changed this file; the cycle-2 check is therefore made against HEAD. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/exclude-attribute-form.2026-10-22T13-25.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/exclude-attribute-form.2026-10-22T13-25.md new file mode 100644 index 000000000..f211f0bad --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/exclude-attribute-form.2026-10-22T13-25.md @@ -0,0 +1,9 @@ +# ExcludeFromCodeCoverage attribute form for the net48 target (P1-T11, #1002 cycle 1) + +Timestamp: 2026-10-22T13-25 +Command: msbuild QuickFiler/QuickFiler.csproj /t:Build /p:Configuration=Debug /p:Platform=AnyCPU with a temporary [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification = "probe")] on ActionOkAsync (probe edit replaced by P1-T12) +EXIT_CODE: 1 +Output Summary: +- Compiler rejected the named property: error CS0246 "The type or namespace name 'Justification' could not be found" at EfcFormController.Actions.cs on the ActionOkAsync attribute. +- The .NET Framework 4.8.1 ExcludeFromCodeCoverageAttribute has no Justification property. +ATTRIBUTE-FORM: COMMENT diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/manual-outlook-check.2026-10-22T10-17.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/manual-outlook-check.2026-10-22T10-17.md new file mode 100644 index 000000000..b7b505ea0 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/manual-outlook-check.2026-10-22T10-17.md @@ -0,0 +1,28 @@ +# Manual Outlook Check (#1002) + +Timestamp: 2026-10-22T10-17 +Status: PENDING HUMAN INTERACTION + +Executed by an agent: no. No agent executed any step below; this record holds the instructions for the maintainer. The check is non-blocking for completion (spec AC26). + +## Why it cannot be automated + +The check needs a live Outlook desktop process with the TaskMaster VSTO add-in loaded, a mailbox whose folder tree has at least three nested levels, and the WebView2 runtime rendering the EFC and QFC breadcrumb surfaces. None of these is available to an unattended agent or to CI, and pixel layout and WebView2 message delivery are not observable from unit tests. + +## Steps (from spec Test Strategy, "Manual validation") + +EFC (Efficient Filing Console): + +1. Open EFC on a mailbox with a three-level folder tree. +2. Expand a suggested folder, then expand a child that has subfolders. +3. Select a grandchild; confirm it is highlighted. +4. Press OK and confirm the filing destination is that grandchild. +5. Reopen EFC and press the trash button; confirm the "Trash to Delete" row is highlighted and that OK targets it. + +QFC (Quick Filer): + +6. Open QFC, expand a suggested folder, then expand a child that has subfolders. +7. Confirm the +/- follows the folder name on every nested row. +8. Confirm each nested level is indented one step further than its visible parent. + +Result: not yet performed. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/preflight-fixes.2026-10-22T09-47.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/preflight-fixes.2026-10-22T09-47.md new file mode 100644 index 000000000..f67c459d0 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/preflight-fixes.2026-10-22T09-47.md @@ -0,0 +1,24 @@ +# Preflight Fixes — Issue #1002 + +Timestamp: 2026-10-22T09-47 +Plan: docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/plan.2026-10-22T05-08.md +Directive: PREFLIGHT VALIDATION ONLY (single pass; no task checked; no task added, removed, or reordered) + +## Fixes applied in task text + +1. [P0-T6] | unsatisfiable-by-ordering evidence (validator-blind) | `scripts/vscode/Invoke-MSTestWithCoverage.ps1` writes its post-processed Cobertura document to the fixed default `coverage\coverage.cobertura.xml`, which the P9-T4 run overwrites; P9-T5 needs the baseline per-line data to compute changed-line coverage against "the baseline coverage of the lines it replaced". Added an instruction to copy the baseline document to `SCRATCH` as `issue-1002-baseline-cobertura.xml` (never committed). The P9-T5 assertion is unchanged. +2. [P3-T4] | count typo | Acceptance named "the eight `RenderRowFragment_` tests"; the spec Test Strategy, P2-T3, and P9-T15 name six. Changed to "six". +3. [P3-T9] | nullable compile defect (validator-blind) | `BreadcrumbInboundMessage.NodeId` is `string?` in a `#nullable enable` file; passing it to a `string` parameter raises CS8604, which TYPECHECK promotes to an error. Task text now specifies `message.NodeId!`, matching the existing `message.Key!` arm in `ProcessInboundAsync`. Behavior unchanged. +4. [P4-T1] | nullable compile defect (validator-blind) | `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` carries no `#nullable enable`; the literal `string? preferredRowText = null` raises CS8632, which TYPECHECK promotes to an error. Literal changed to `string preferredRowText = null`. Behavior unchanged. +5. [P9-T10] | ambiguous test reference | "the four `DescendantToggle_` router tests" matched five spec names (`DescendantToggle_LeafNode_IsRejectedWithoutProviderCall` belongs to AC6). Replaced with the four explicit test names. +6. [P9-T33] | unsatisfiable anchor (validator-blind) | "every criterion text unchanged against the merge base": `spec.md` was added on this branch and does not exist at the merge base of `origin/main` and `HEAD`. Changed the comparison to each AC line, with the checkbox marker normalized, against the same line of `spec.md` at `HEAD`. The asserted property (criterion text unchanged) is unchanged. + +## Rule check on the fixes + +- No fix cites a line number or commit identifier (G11, G12). +- No fix records a host path; the scratch copy is named by the `SCRATCH` token. +- No fix adds, removes, or reorders a task; every assertion keeps its meaning. + +## Re-validation + +- `mcp__drm-copilot__validate_orchestration_artifacts` (artifact_type `plan`) run after the fixes. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/preflight-fixes.2026-10-22T13-49.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/preflight-fixes.2026-10-22T13-49.md new file mode 100644 index 000000000..f92ae26ac --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/preflight-fixes.2026-10-22T13-49.md @@ -0,0 +1,6 @@ +Timestamp: 2026-10-22T13-49 +Plan: docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T13-43.md + +Fixes: + +1. Phase 3 preamble: the sentence said the `_globals.Ol` argument is evaluated first. In `BindBreadcrumbRowsAsync` the score projection is computed first, but it contains no await. Reworded to state that the `_globals.Ol` argument throws before the `BindRowsAsync` call is made and that the preceding projection has no await. Intent and assertions unchanged. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/seam-build-efc-descendants.2026-10-22T09-55.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/seam-build-efc-descendants.2026-10-22T09-55.md new file mode 100644 index 000000000..78a4f3562 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/seam-build-efc-descendants.2026-10-22T09-55.md @@ -0,0 +1,9 @@ +# Seam Build: Group A Declaration-Only Seams (#1002) + +Timestamp: 2026-10-22T09-55 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: +- `0 Error(s)`, `0 Warning(s)`. +- Seams compiled: `BreadcrumbMessageTypes.DescendantToggle` ("descendantToggle"), `BreadcrumbMessageTypes.DescendantActivate` ("descendantActivate"), optional trailing `string? nodeId = null` exposed as `BreadcrumbInboundMessage.NodeId`; `BreadcrumbRow` made `partial`; new `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` (registered in `UtilitiesCS/UtilitiesCS.csproj`) declares `SetLeafDescendants`, `TryBeginDescendantExpand`, `TryCollapseDescendant`, `TryAttachDescendantChildren`, `TryAbandonDescendantExpand` (each false), `GetDescendantPath` (null), `SelectedDescendantId` (null), `TrySelectDescendant` (false), and internal `DescendantNodes` (empty). +- `BreadcrumbMessageCodec.cs` and `BreadcrumbDescendantNode.cs` not edited; every existing constructor call compiled unchanged. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/seam-build-efc-trash.2026-10-22T10-09.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/seam-build-efc-trash.2026-10-22T10-09.md new file mode 100644 index 000000000..f16519a2e --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/seam-build-efc-trash.2026-10-22T10-09.md @@ -0,0 +1,9 @@ +# Seam Build: Group B Declaration-Only Seam (#1002) + +Timestamp: 2026-10-22T10-09 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: +- `0 Error(s)`, `0 Warning(s)`. +- Seam: internal `BreadcrumbBridgeRouter.BindRowsAsync` gains optional trailing `string? preferredRowText = null` (with its XML `param`) and calls `SelectPreferredRow(preferredRowText)` immediately before `DeliverDocument()`; `private void SelectPreferredRow(string? preferredRowText)` is declared in `BreadcrumbBridgeRouter.Selection.cs` with an empty body; `EfcFormController.BindBreadcrumbRowsAsync` gains optional `string preferredRowText = null` (no `?`, because that file has no `#nullable enable`) passed through to the router. +- Every existing caller of both bind methods compiled unchanged (public three-argument overload, internal four-argument calls, and the controller one-argument calls). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/ac-reconciliation.2026-10-22T10-28.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/ac-reconciliation.2026-10-22T10-28.md new file mode 100644 index 000000000..b9c505a1a --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/ac-reconciliation.2026-10-22T10-28.md @@ -0,0 +1,20 @@ +# QA Gate: Acceptance-Criteria Reconciliation (#1002) + +Timestamp: 2026-10-22T10-28 +Command: grep -cE '^- \[x\] AC[0-9]+ ' spec.md; grep -cE '^- \[ \] AC[0-9]+ ' spec.md; git diff HEAD -- /spec.md | grep -E '^[-+]- \[' | sed -E 's/^[-+]- \[.\] //' | sort | uniq -c +EXIT_CODE: 0 +Output Summary: +- Checked AC boxes: 26; unchecked: 0. +- Text check against `spec.md` at `HEAD` (the merge base has no `spec.md`): the diff changes exactly 26 lines, and after the checkbox marker is normalized each of the 26 criterion texts appears exactly twice (once removed, once added), so every criterion text is unchanged and only the box changed. +- Each AC was checked off by its own task (P9-T7 to P9-T32), one edit per criterion, citing the evidence named in the plan. + +### Acceptance Criteria Status +- Source: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md` +- Total AC items: 26 +- Checked off (delivered): 26 +- Remaining (unchecked): 0 +- Items remaining: none + +Notes carried into the self-review: +- AC18: the merge-base diff also lists three orchestrator-owned paths committed before execution (two task-researcher agent-memory files and the promotion record); none is a spec-excluded file. See `scope-fences`. +- AC26 is satisfied by the pending-human record; the manual Outlook check itself has not been performed. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b1-per-file-coverage.2026-10-22T13-33.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b1-per-file-coverage.2026-10-22T13-33.md new file mode 100644 index 000000000..ed62a7be9 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b1-per-file-coverage.2026-10-22T13-33.md @@ -0,0 +1,9 @@ +# B-1 verification (P4-T7, #1002 cycle 1) + +Timestamp: 2026-10-22T13-33 +Command: pwsh -NoProfile -File SCRATCH/issue-1002-r-perfile.ps1 -Files EfcFormController.Breadcrumb.cs -Classes (union of class line nodes of the post-processed Cobertura) +EXIT_CODE: 0 +Output Summary: +- QuickFiler/Controllers/EfcFormController.Breadcrumb.cs: lines 93/101 (92.08%), branches 34/42 (80.95%). +- Threshold: covered lines at least 84 (93 observed), covered branches at least 24 (34 observed). Met. +- Baseline 78/121 lines, 22/42 branches. The valid-line count fell from 121 to 101 because ConfigureBreadcrumbControl now carries [ExcludeFromCodeCoverage]. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b2-per-file-coverage.2026-10-22T13-33.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b2-per-file-coverage.2026-10-22T13-33.md new file mode 100644 index 000000000..27708122d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b2-per-file-coverage.2026-10-22T13-33.md @@ -0,0 +1,8 @@ +# B-2 verification (P4-T8, #1002 cycle 1) + +Timestamp: 2026-10-22T13-33 +Command: pwsh -NoProfile -File SCRATCH/issue-1002-r-perfile.ps1 -Files BreadcrumbMessages.cs -ThrowLines +EXIT_CODE: 0 +Output Summary: +- condition-coverage on each `throw new ArgumentNullException` line of UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs: lines 84, 85, 124, 144, 173, 174, 175 each 2/2 (100%). Seven lines. +- File totals: branches 14/14 (100.00%), lines 32/32 (100.00%). Threshold at least 11 of 14: met. Baseline 7/14. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b3-per-file-coverage.2026-10-22T13-33.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b3-per-file-coverage.2026-10-22T13-33.md new file mode 100644 index 000000000..387745bf6 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b3-per-file-coverage.2026-10-22T13-33.md @@ -0,0 +1,11 @@ +# B-3 verification (P4-T9, #1002 cycle 1) + +Timestamp: 2026-10-22T13-33 +Command: pwsh -NoProfile -File SCRATCH/issue-1002-r-perfile.ps1 -Files EfcFormController.Actions.cs,EfcFormController.Breadcrumb.cs -Classes +EXIT_CODE: 0 +Output Summary: +- QuickFiler/Controllers/EfcFormController.Actions.cs: lines 26/28 (92.86%), branches 13/16 (81.25%). Baseline 26/111 (23.42%), 12/36 (33.33%). +- QuickFiler/Controllers/EfcFormController.Breadcrumb.cs: lines 93/101 (92.08%), branches 34/42 (80.95%). Baseline 78/121 (64.46%), 22/42 (52.38%). +- Both files meet at least 80% line and at least 75% branch. +- Class nodes remaining for these files in the post-processed Cobertura: one per file, `QuickFiler.Controllers.EfcFormController` (Actions.cs covered 26 valid 28; Breadcrumb.cs covered 93 valid 101). No residual compiler-generated state-machine or lambda-closure class nodes appear, so no shortfall exists and no exclusion beyond the five named members was added. +- Exemption form: member-level [ExcludeFromCodeCoverage] (parameterless attribute with adjacent comment, because the .NET Framework 4.8.1 attribute has no Justification property; see exclude-attribute-form artifact) on ActionOkAsync, ActionCancelAsync, CreateFolderAsync, RefreshSuggestionsAsync, ConfigureBreadcrumbControl. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b3-refresh-suggestions-decision.2026-10-22T13-24.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b3-refresh-suggestions-decision.2026-10-22T13-24.md new file mode 100644 index 000000000..f5f4b43de --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b3-refresh-suggestions-decision.2026-10-22T13-24.md @@ -0,0 +1,11 @@ +# B-3 sequencing decision for RefreshSuggestionsAsync (P1-T10, #1002 cycle 1) + +Timestamp: 2026-10-22T13-24 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . ; per-line union of Cobertura class line nodes for EfcFormController.Actions.cs restricted to the body of RefreshSuggestionsAsync +EXIT_CODE: 0 +Output Summary: +- Run executed after the B-1 and CR-4 tests were added: exit 0, all tests passed. +- RefreshSuggestionsAsync (method body including the Task.Run lambdas and the async state machine, which Cobertura attributes to the single EfcFormController class node of this file): 0 covered of 9 valid lines (every line reported hits 0). +- EfcFormController.Breadcrumb.cs after B-1 tests: lines 93/121 (76.86%), branches 34/42 (80.95%). EfcFormController.Actions.cs: lines 26/111 (23.42%), branches 13/36 (36.11%). +- Decision: RefreshSuggestionsAsync remains fully uncovered because it dereferences the WinForms viewer search box and the Outlook-bound data model. +ATTRIBUTE-REFRESHSUGGESTIONS: YES diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b4-grep.2026-10-22T13-33.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b4-grep.2026-10-22T13-33.md new file mode 100644 index 000000000..05a1d7413 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/b4-grep.2026-10-22T13-33.md @@ -0,0 +1,7 @@ +# B-4 verification (P4-T10, #1002 cycle 1) + +Timestamp: 2026-10-22T13-33 +Command: git grep -c -e aria-selected -- UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs +EXIT_CODE: 0 +Output Summary: +- Success-case output: `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs:2`. Count 2 (light and dark rules); threshold at least 2 met. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-comparison.2026-10-22T10-26.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-comparison.2026-10-22T10-26.md new file mode 100644 index 000000000..a78c771a3 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-comparison.2026-10-22T10-26.md @@ -0,0 +1,63 @@ +# QA Gate: Coverage Comparison (#1002) + +Timestamp: 2026-10-22T10-26 +Command: pwsh -NoProfile -File SCRATCH/issue-1002-covcompare.ps1 -Baseline SCRATCH/issue-1002-baseline-cobertura.xml -Final SCRATCH/issue-1002-final-cobertura.xml; pwsh -NoProfile -File SCRATCH/issue-1002-methods.ps1 (per-method lines, including async MoveNext state machines); git diff --stat $(git merge-base origin/main HEAD) -- coverage.config; git diff $(git merge-base origin/main HEAD) -- '*.cs' | grep '^+' | grep -c ExcludeFromCodeCoverage +EXIT_CODE: 0 +Output Summary: no shortfall. Repository line and branch coverage rose; both new files and every new method are at 100 percent line coverage; every changed-line figure is 100 percent and not below the baseline coverage of the lines it replaced; no coverage exclusion was added. + +## Repository figures (post-processed first-party Cobertura) + +| Measure | Baseline (P0-T6) | Final (P9-T4, loop pass 3) | Floor | Result | +|---|---|---|---|---| +| Line | 57272/66515 = 86.10% | 57523/66780 = 86.14% | 80% | PASS, not lowered | +| Branch | 13923/17337 = 80.31% | 14016/17439 = 80.37% | 75% | PASS, not lowered | + +## New files (line coverage, final) + +| File | Covered/Valid | +|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` | 82/82 = 100.00% | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | 63/63 = 100.00% | + +## New methods in modified and new files (line coverage, final) + +| Method | File | Covered/Valid | +|---|---|---| +| SetLeafDescendants | BreadcrumbRow.Descendants.cs | 17/17 = 100.00% | +| HandleDescendantToggleAsync | BreadcrumbBridgeRouter.Descendants.cs | 53/53 = 100.00% (async state machine MoveNext, lines 22 to 88) | +| ActivateDescendant | BreadcrumbBridgeRouter.Descendants.cs | 10/10 = 100.00% | +| TryGetRowSelection | BreadcrumbBridgeRouter.Selection.cs | 24/24 = 100.00% | +| SelectPreferredRow | BreadcrumbBridgeRouter.Selection.cs | 17/17 = 100.00% | +| PresentsText (helper) | BreadcrumbBridgeRouter.Selection.cs | 8/8 = 100.00% | +| ApplyDeleteGestureAsync | EfcFormController.Actions.cs | 7/7 = 100.00% (async state machine MoveNext, lines 100 to 108) | +| AppendDescendants (helper) | BreadcrumbHtmlRenderer.cs | 37/37 = 100.00% | + +Remediation within this loop: the pass-2 figures were `SetLeafDescendants` 15/17 = 88.24 percent and `SelectPreferredRow` 15/17 = 88.24 percent. The two tests named in the pass-3 format artifact were added, and the loop was re-run from P9-T1. + +## Changed lines per modified production file + +Changed lines are the added or modified lines of the merge-base diff (`git diff -U0` new-side hunks) that Cobertura instruments in the final run; replaced lines are the old-side hunk lines that Cobertura instrumented in the baseline run. + +| File | Changed (final) | Replaced (baseline) | Result | +|---|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | 8/8 = 100.00% | none instrumented | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | 3/3 = 100.00% | 1/1 = 100.00% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | 15/15 = 100.00% | 2/2 = 100.00% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | 49/49 = 100.00% | 3/3 = 100.00% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | not instrumented (string constants only) | not instrumented | N/A | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 5/5 = 100.00% | none instrumented | PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | 7/7 = 100.00% | 1/1 = 100.00% | PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | 48/48 = 100.00% | 17/17 = 100.00% | PASS | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | 5/5 = 100.00% | 2/2 = 100.00% | PASS | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | 7/7 = 100.00% | 1/1 = 100.00% | PASS | + +Whole-file final line coverage for context: BreadcrumbRow.cs 98.77%, BreadcrumbHtmlRenderer.cs 97.77%, BreadcrumbBridgeRouter.cs 99.19%, .Arrows.cs 96.40%, .Selection.cs 98.37%, EfcFormController.Actions.cs 23.42% (baseline 21.30%), EfcFormController.Breadcrumb.cs 64.46% (baseline 78/115 = 67.83%, final 78/121). + +Observation on `EfcFormController.Breadcrumb.cs`: the whole-file figure fell because the unchanged private method `BindFolderRows` lost its only covered caller. At baseline the delete gesture called it (6 of its 7 lines covered through `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows`); the delete gesture now awaits `BindBreadcrumbRowsAsync` directly, as P5-T2 specifies, so `BindFolderRows` is reached only through `BindSourceFolderRows`, which no unit test exercises at baseline or now. The six lines are unchanged lines, so the changed-line rule is met, and the repository figures rose. This is recorded as an observation for review, not as a shortfall against the AC23 criteria. + +## Exclusions + +- `coverage.config`, `TaskMaster.runsettings`, `scripts/vscode/TaskMaster.cli.runsettings`: no diff from the merge base. +- `ExcludeFromCodeCoverage` added in the tracked diff: 0; in the new files: 0. + +`FolderBreadcrumb.js` is measured by the node runner: 100.00 percent line coverage before and after (see `baseline-node-suites` and `node-suites-run`). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-comparison.2026-10-22T13-33.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-comparison.2026-10-22T13-33.md new file mode 100644 index 000000000..4bc78e5bc --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-comparison.2026-10-22T13-33.md @@ -0,0 +1,21 @@ +# Remediation coverage comparison (P4-T12, #1002 cycle 1) + +Timestamp: 2026-10-22T13-33 +Command: SCRATCH/issue-1002-r-perfile.ps1 on the baseline (P0-T4) and final (P4-T5) post-processed Cobertura documents; first-party totals from the coverage script summary line +EXIT_CODE: 0 +Output Summary: + +| Item | Baseline | Final | Delta | +|---|---|---|---| +| B-1 EfcFormController.Breadcrumb.cs lines | 78/121 (64.46%) | 93/101 (92.08%) | +15 covered, +27.62 pts | +| B-1 EfcFormController.Breadcrumb.cs branches | 22/42 (52.38%) | 34/42 (80.95%) | +12 covered, +28.57 pts | +| B-2 BreadcrumbMessages.cs branches | 7/14 (50.00%) | 14/14 (100.00%) | +7 covered, +50.00 pts | +| B-3/CR-4 EfcFormController.Actions.cs lines | 26/111 (23.42%) | 26/28 (92.86%) | 0 covered, valid 111 to 28 | +| B-3/CR-4 EfcFormController.Actions.cs branches | 12/36 (33.33%) | 13/16 (81.25%) | +1 covered, valid 36 to 16 | +| Repository first-party lines | 57527/66780 (86.14%) | 57541/66677 (86.30%) | +0.16 pts | +| Repository first-party branches | 14018/17439 (80.38%) | 14036/17419 (80.58%) | +0.20 pts | + +- Residual compiler-generated classes (P4-T9): none; one class node per file. +- Repository totals are not lowered and exceed the 80% line and 75% branch floors. +- B-3 exemption is recorded as member-level [ExcludeFromCodeCoverage] attributes only (five named members); no coverage.config entry (`git diff --stat` of coverage.config against the merge base is empty). +- The earlier coverage-comparison.2026-10-22T10-26.md is not edited. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-xml-refresh.2026-10-22T13-32.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-xml-refresh.2026-10-22T13-32.md new file mode 100644 index 000000000..c20038084 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-xml-refresh.2026-10-22T13-32.md @@ -0,0 +1,9 @@ +# Canonical coverage artifact refresh (P4-T6, #1002 cycle 1) + +Timestamp: 2026-10-22T13-32 +Command: copy of coverage/coverage.cobertura.xml to artifacts/csharp/coverage.xml (same derivation as the earlier executor final artifact: byte-identical copy of the post-processed Cobertura); cmp reports identical +EXIT_CODE: 0 +Output Summary: +- Source: coverage/coverage.cobertura.xml (post-processed Cobertura of the P4-T5 run). Destination: artifacts/csharp/coverage.xml (gitignored, not staged). +- Destination modification time is later than the P4-T5 trx and Cobertura write times. +- First-party totals: lines 57541/66677 (86.30%), branches 14036/17419 (80.58%). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-analyzers.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-analyzers.2026-10-22T14-06.md new file mode 100644 index 000000000..33590b205 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-analyzers.2026-10-22T14-06.md @@ -0,0 +1,7 @@ +# Cycle 2 P5-T3: analyzers rebuild + +Timestamp: 2026-10-22T14-06 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: +- Build succeeded. 0 Warning(s), 0 Error(s). Time Elapsed 00:00:11.45. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-coverage-comparison.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-coverage-comparison.2026-10-22T14-06.md new file mode 100644 index 000000000..d5dffabe9 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-coverage-comparison.2026-10-22T14-06.md @@ -0,0 +1,19 @@ +# Cycle 2 P5-T7: coverage comparison + +Timestamp: 2026-10-22T14-06 +Command: SCRATCH/issue-1002-r2-perfile.ps1 -Files EfcFormController.Actions.cs,BreadcrumbDocumentAssets.cs on the P0-T3 and P5-T5 post-processed Cobertura documents +EXIT_CODE: 0 +Output Summary: + +| Item | Baseline | Final | Delta | +|---|---|---|---| +| EfcFormController.Actions.cs lines | 26/28 (92.86%) | 26/28 (92.86%) | 0 | +| EfcFormController.Actions.cs branches | 13/16 (81.25%) | 13/16 (81.25%) | 0 | +| BreadcrumbDocumentAssets.cs lines | 0/0 (no instrumented line) | 0/0 | 0 | +| BreadcrumbDocumentAssets.cs branches | 0/0 | 0/0 | 0 | +| Repository first-party lines | 57542/66677 (86.30%) | 57546/66677 (86.31%) | +4 covered | +| Repository first-party branches | 14038/17419 (80.59%) | 14038/17419 (80.59%) | 0 | + +- Repository line and branch totals are not lowered. +- EfcFormController.Actions.cs meets 80% line and 75% branch. BreadcrumbDocumentAssets.cs holds only string constants: the static class has no executable member, no class node exists in the Cobertura document, and the floor is not evaluable (recorded as measured, not adjusted). The change adds string constant text only, so no executable line was added or uncovered. +- No exclusion added; earlier comparisons are not edited. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-coverage-xml-refresh.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-coverage-xml-refresh.2026-10-22T14-06.md new file mode 100644 index 000000000..9e94da5ec --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-coverage-xml-refresh.2026-10-22T14-06.md @@ -0,0 +1,9 @@ +# Cycle 2 P5-T6: canonical coverage artifact refresh + +Timestamp: 2026-10-22T14-06 +Command: copy of coverage/coverage.cobertura.xml to artifacts/csharp/coverage.xml (same derivation as coverage-xml-refresh.2026-10-22T13-32.md); cmp reports identical +EXIT_CODE: 0 +Output Summary: +- Source: coverage/coverage.cobertura.xml (post-processed Cobertura of the P5-T5 run). Destination: artifacts/csharp/coverage.xml (gitignored, not staged). +- Destination modification time (10:06:14) is later than the P5-T5 trx (10:05:24) and Cobertura (10:05:45) write times. +- First-party totals: lines 57546/66677 (86.31%), branches 14038/17419 (80.59%). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes-n3.2026-10-22T14-00.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes-n3.2026-10-22T14-00.md new file mode 100644 index 000000000..9e0914ff7 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes-n3.2026-10-22T14-00.md @@ -0,0 +1,7 @@ +# Cycle 2 P3-T5: file size after N-3 + +Timestamp: 2026-10-22T14-00 +Command: wc -l QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs +EXIT_CODE: 0 +Output Summary: +- EfcFormControllerTests.Issue1002.cs: 339 physical lines after formatting (at or under 500). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes-n5.2026-10-22T14-03.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes-n5.2026-10-22T14-03.md new file mode 100644 index 000000000..7983c8dc4 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes-n5.2026-10-22T14-03.md @@ -0,0 +1,9 @@ +# Cycle 2 P4-T6: file sizes after N-5 + +Timestamp: 2026-10-22T14-03 +Command: wc -l on each path +EXIT_CODE: 0 +Output Summary: +- BreadcrumbHtmlRendererIssue1002Tests.cs: 368 +- BreadcrumbDocumentAssets.cs: 185 +- Both at or under 500. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes.2026-10-22T14-06.md new file mode 100644 index 000000000..47ffe09d5 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-file-sizes.2026-10-22T14-06.md @@ -0,0 +1,11 @@ +# Cycle 2 P5-T8: file sizes after final format + +Timestamp: 2026-10-22T14-06 +Command: wc -l on each edited .cs file +EXIT_CODE: 0 +Output Summary: +- EfcFormControllerTests.Issue1002.cs: 339 +- BreadcrumbHtmlRendererIssue1002Tests.cs: 368 +- BreadcrumbDocumentAssets.cs: 185 +- EfcFormController.Actions.cs: 206 +- All at or under 500. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-format-check.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-format-check.2026-10-22T14-06.md new file mode 100644 index 000000000..4e83a1e5a --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-format-check.2026-10-22T14-06.md @@ -0,0 +1,7 @@ +# Cycle 2 P5-T2: format check + +Timestamp: 2026-10-22T14-06 +Command: dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: +- Printed "Checked 1675 files in 4969ms."; exit 0. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-format.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-format.2026-10-22T14-06.md new file mode 100644 index 000000000..d138aff8a --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-format.2026-10-22T14-06.md @@ -0,0 +1,7 @@ +# Cycle 2 P5-T1: format + +Timestamp: 2026-10-22T14-06 +Command: dotnet tool run csharpier format . +EXIT_CODE: 0 +Output Summary: +- Printed "Formatted 1675 files in 1534ms."; git status --porcelain (non-docs) before and after the formatter is identical: it changed no tracked file beyond this plan's file set (EfcFormController.Actions.cs, EfcFormControllerTests.Issue1002.cs, BreadcrumbHtmlRendererIssue1002Tests.cs, BreadcrumbDocumentAssets.cs). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-n1-verification.2026-10-22T13-57.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-n1-verification.2026-10-22T13-57.md new file mode 100644 index 000000000..606278f47 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-n1-verification.2026-10-22T13-57.md @@ -0,0 +1,10 @@ +# Cycle 2 P2-T2: N-1 verification + +Timestamp: 2026-10-22T13-57 +Command: grep -c MessageBox QuickFiler/Controllers/EfcFormController.Actions.cs (before and after); git diff HEAD -U0 -- the file; git diff 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- '*.cs' | grep '^+' | grep -c ExcludeFromCodeCoverage +EXIT_CODE: 0 +Output Summary: +- MessageBox occurrence count: before 5, after 4 (drops by exactly one). +- Reworded comment, second line: "this handler awaits the concrete EfcViewer UiSyncContext and calls Close on the live WinForms viewer, which cannot run headless." It contains UiSyncContext and Close, not MessageBox; the attribute token is not in the comment. +- The diff against HEAD is one replaced comment line; attribute, signature and body are untouched. +- Added ExcludeFromCodeCoverage lines against the merge base: 5 (still 5). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-r1-final.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-r1-final.2026-10-22T14-06.md new file mode 100644 index 000000000..8d7a17d5e --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-r1-final.2026-10-22T14-06.md @@ -0,0 +1,9 @@ +# Cycle 2 P5-T10: R-1 final verification + +Timestamp: 2026-10-22T14-06 +Command: git diff HEAD --stat -- spec.md; git diff HEAD -U0 -- spec.md; git diff 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- '*.cs' | grep '^+' | grep -c ExcludeFromCodeCoverage; git diff --stat 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- coverage.config +EXIT_CODE: 0 +Output Summary: +- spec.md diff against HEAD: 5 insertions, 1 deletion in two hunks (the AC23 clause line and the appended Planner Amendments section); AC22 and AC24 unchanged; AC23 still checked. +- Added ExcludeFromCodeCoverage lines against the merge base: 5. +- coverage.config stat against the merge base: empty. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-r1-verification.2026-10-22T13-56.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-r1-verification.2026-10-22T13-56.md new file mode 100644 index 000000000..66eb67bbd --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-r1-verification.2026-10-22T13-56.md @@ -0,0 +1,11 @@ +# Cycle 2 P1-T3: R-1 verification + +Timestamp: 2026-10-22T13-56 +Command: git diff HEAD --stat -- spec.md; git diff HEAD -U0 -- spec.md; git diff 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- '*.cs' | grep '^+' | grep -c ExcludeFromCodeCoverage; git diff --stat 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- coverage.config; git status --porcelain +EXIT_CODE: 0 +Output Summary: +- spec.md stat: 1 file changed, 5 insertions(+), 1 deletion(-). Hunks: exactly two, the AC23 line and the appended Planner Amendments section. +- AC22 and AC24 lines are byte-identical to their pre-edit text (diff of the pre-edit copy against the edited file is empty); AC23 still starts with the checked marker. +- Added ExcludeFromCodeCoverage lines in *.cs against the merge base: 5; members named: ActionCancelAsync ActionOkAsync ConfigureBreadcrumbControl CreateFolderAsync RefreshSuggestionsAsync +- git diff --stat of coverage.config against the merge base: empty. +- git status --porcelain (untracked and modified listing at this point): spec.md modified; cycle2 evidence files untracked under the feature folder. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-scope-fences.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-scope-fences.2026-10-22T14-06.md new file mode 100644 index 000000000..98bf7d868 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-scope-fences.2026-10-22T14-06.md @@ -0,0 +1,12 @@ +# Cycle 2 P5-T9: scope fences + +Timestamp: 2026-10-22T14-06 +Command: git diff --name-only HEAD; git diff --name-only 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7; git status --porcelain; git diff 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- '*.cs' | grep '^+' | grep -c ExcludeFromCodeCoverage; git diff --stat 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- coverage.config; search of QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs for ConfigureAwait(false) +EXIT_CODE: 0 +Output Summary: +- Files changed this cycle (git diff --name-only HEAD): QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs; QuickFiler/Controllers/EfcFormController.Actions.cs; UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs; UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs; the feature spec.md and this remediation plan. The merge-base listing additionally contains the earlier committed branch files; none is edited this cycle beyond the list above. +- git status --porcelain: the six modified files above plus untracked cycle2 evidence Markdown files under the feature folder; no file on the spec's excluded list, under .claude/rules/ or .github/instructions/, and no .trx, .coverage or Cobertura document is listed. +- Added ExcludeFromCodeCoverage lines in *.cs against the merge base: 5. +- coverage.config: git diff --stat against the merge base is empty. +- ConfigureAwait(false) in BreadcrumbBridgeRouter*.cs: no hit. +- The executor did not stage or commit. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-tests.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-tests.2026-10-22T14-06.md new file mode 100644 index 000000000..6479de4c1 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-tests.2026-10-22T14-06.md @@ -0,0 +1,10 @@ +# Cycle 2 P5-T5: tests with coverage + +Timestamp: 2026-10-22T14-06 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +EXIT_CODE: 0 +Output Summary: +- Test run outcome Completed: total 7599, executed 7599, passed 7599, failed 0 (error, timeout, aborted, notExecuted, inconclusive 0). +- P0-T3 baseline passed 7597; delta +2 = BindFolderRows_FaultBeforeDelivery_FaultsDeliveredTaskPromptly and BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan. +- First-party coverage: lines 57546/66677 (86.31%), branches 14038/17419 (80.59%). +- No raw trx or Cobertura committed. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-typecheck.2026-10-22T14-06.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-typecheck.2026-10-22T14-06.md new file mode 100644 index 000000000..fe8f0a858 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/cycle2-typecheck.2026-10-22T14-06.md @@ -0,0 +1,7 @@ +# Cycle 2 P5-T4: type-check rebuild + +Timestamp: 2026-10-22T14-06 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +EXIT_CODE: 0 +Output Summary: +- Build succeeded. 0 Warning(s), 0 Error(s). No /p:Nullable=enable. Time Elapsed 00:00:11.30. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/evidence-form.2026-10-22T10-28.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/evidence-form.2026-10-22T10-28.md new file mode 100644 index 000000000..452740445 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/evidence-form.2026-10-22T10-28.md @@ -0,0 +1,10 @@ +# QA Gate: Evidence Form (#1002) + +Timestamp: 2026-10-22T10-28 +Command: pwsh -NoProfile -File SCRATCH/issue-1002-evidenceform.ps1 (git diff --name-status $(git merge-base origin/main HEAD) added paths plus git status --porcelain --untracked-files=all untracked paths; extension and name checks; Get-ChildItem over /evidence; Select-String for drive-rooted paths and user-profile segments) +EXIT_CODE: 0 +Output Summary: +- Added or untracked paths examined: 52. Paths ending in `.trx`, `.coverage`, or `.xml`, or containing `cobertura` or `jacoco`: 0. +- Files under `/evidence/`: 35 at the time of the check, all Markdown (non-Markdown: 0). +- Search of `/evidence/` for a drive-rooted path (a drive letter, a colon, and a separator), a user-profile directory segment, the account name, the short profile name, or the per-user application-data folder name: 0 hits (patterns held in SCRATCH/issue-1002-evidenceform.ps1). A re-run after the self-review was written also returned 0 hits once this line was reworded, because the earlier wording quoted the searched tokens and matched itself. +- Raw trx, Cobertura, and JaCoCo outputs remain in the ignored `coverage/` folder and in SCRATCH only. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes-remediation.2026-10-22T13-22.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes-remediation.2026-10-22T13-22.md new file mode 100644 index 000000000..fc1c27b8d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes-remediation.2026-10-22T13-22.md @@ -0,0 +1,7 @@ +# File size after P1 (P1-T9, #1002 cycle 1) + +Timestamp: 2026-10-22T13-22 +Command: @(Get-Content -LiteralPath QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs).Count +EXIT_CODE: 0 +Output Summary: +- QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs: 313 physical lines after formatting (at or under 500). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes-remediation.2026-10-22T13-33.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes-remediation.2026-10-22T13-33.md new file mode 100644 index 000000000..dad109a91 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes-remediation.2026-10-22T13-33.md @@ -0,0 +1,13 @@ +# File sizes after the final format pass (P4-T11, #1002 cycle 1) + +Timestamp: 2026-10-22T13-33 +Command: line count of each file below after the final csharpier format +EXIT_CODE: 0 +Output Summary: +- QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs: 313 +- UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs: 98 +- UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs: 343 +- UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs: 182 +- QuickFiler/Controllers/EfcFormController.Actions.cs: 206 +- QuickFiler/Controllers/EfcFormController.Breadcrumb.cs: 219 +- Every count is at or under 500. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes.2026-10-22T10-16.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes.2026-10-22T10-16.md new file mode 100644 index 000000000..51207e670 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes.2026-10-22T10-16.md @@ -0,0 +1,40 @@ +# QA Gate: File Sizes (#1002) + +Timestamp: 2026-10-22T10-16 +Command: pwsh: foreach file, @(Get-Content -LiteralPath ).Count (SCRATCH/issue-1002-linecount.ps1); git diff $(git merge-base origin/main HEAD) -- QuickFiler/Controllers/BreadcrumbBridgeRouter.cs +EXIT_CODE: 0 +Output Summary: every new or modified C# file and `FolderBreadcrumb.js` is at most 500 physical lines; `BreadcrumbBridgeRouter.cs` gains only the two delegating case arms and the preferred-row parameter (with its documentation and its call); none of the seven near-limit test files is in the change set. + +| File | Baseline (P0-T7) | Now | +|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | 361 | 370 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` | new | 199 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | 173 | 197 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | 203 | 220 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | 234 | 294 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | 149 | 179 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 459 | 471 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | new | 95 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | 211 | 222 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | 236 | 305 | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | 184 | 193 | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | 208 | 215 | +| `QuickFiler/Resources/FolderBreadcrumb.js` | 347 | 350 | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` | new | 108 | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` | new | 449 | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` | new | 278 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` | new | 459 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs` | new | 244 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs` | new | 131 | +| `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | new | 140 | +| `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` | new | 101 | + +## `BreadcrumbBridgeRouter.cs` merge-base diff (summary) + +Three hunks, additions only: the `` documentation (4 lines) and the trailing parameter `string? preferredRowText = null` on the internal `BindRowsAsync`; the call `SelectPreferredRow(preferredRowText);` before `DeliverDocument();`; and two case arms in `ProcessInboundAsync` delegating to `HandleDescendantToggleAsync(row, message.NodeId!)` and `ActivateDescendant(row, message.NodeId!)`. No other logic. + +## Near-limit test files + +`EfcFormControllerTests.cs` (485), `EfcFormControllerTests.Part2.cs` (490), `BreadcrumbBridgeRouterTests.cs` (462), `BreadcrumbBridgeRouterQueueTests.cs` (462), `BreadcrumbBridgeRouterScoreJoinTests.cs` (463), `BreadcrumbBridgeRouterIssue439Tests.cs` (455), `FolderBreadcrumbAssetContractTests.cs` (415): counts unchanged from baseline, and none appears in the P8-T1 path list. + +Counts are taken before the Phase 9 format pass; P9-T1 records whether that pass rewrote any file. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes.2026-10-22T10-28.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes.2026-10-22T10-28.md new file mode 100644 index 000000000..700db37ac --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/file-sizes.2026-10-22T10-28.md @@ -0,0 +1,34 @@ +# QA Gate: File Sizes, Post-Format Recount (#1002) + +Timestamp: 2026-10-22T10-28 +Command: pwsh: foreach file, @(Get-Content -LiteralPath ).Count (SCRATCH/issue-1002-linecount.ps1) +EXIT_CODE: 0 +Output Summary: recount after the Phase 9 format pass and the two coverage-remediation tests; supersedes the 2026-10-22T10-16 counts. Every new or modified C# file and `FolderBreadcrumb.js` is at most 500 physical lines; the seven near-limit test files are unchanged and absent from the change set. The `BreadcrumbBridgeRouter.cs` diff is unchanged from the 10-16 record (two case arms, the preferred-row parameter with its documentation, and its call). + +| File | Baseline (P0-T7) | Now | +|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | 361 | 370 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` | new | 200 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | 173 | 197 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | 203 | 220 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | 234 | 298 | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | 149 | 179 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 459 | 471 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | new | 104 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | 211 | 222 | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | 236 | 305 | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | 184 | 193 | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | 208 | 215 | +| `QuickFiler/Resources/FolderBreadcrumb.js` | 347 | 350 | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` | new | 108 | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` | new | 466 | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` | new | 297 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` | new | 487 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs` | new | 259 | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs` | new | 155 | +| `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | new | 140 | +| `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` | new | 101 | + +Near-limit test files (unchanged): EfcFormControllerTests.cs 485, EfcFormControllerTests.Part2.cs 490, BreadcrumbBridgeRouterTests.cs 462, BreadcrumbBridgeRouterQueueTests.cs 462, BreadcrumbBridgeRouterScoreJoinTests.cs 463, BreadcrumbBridgeRouterIssue439Tests.cs 455, FolderBreadcrumbAssetContractTests.cs 415. + +`QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` (487) is close to the limit; it also holds the shared `EfcDescendantRouterFixture`, so a later addition belongs in a new file. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T10-19.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T10-19.md new file mode 100644 index 000000000..0b7172f31 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T10-19.md @@ -0,0 +1,9 @@ +# Final Toolchain: ANALYZE (#1002) + +Timestamp: 2026-10-22T10-19 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: +- Loop pass 2. `0 Error(s)`, `0 Warning(s)`; Time Elapsed 00:00:14.53. +- Warnings whose path is a Write Set file: none (no warning lines in the log). +- MSBuild resolved through vswhere; raw log kept under SCRATCH as `issue-1002-final-analyze.log`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T10-23.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T10-23.md new file mode 100644 index 000000000..536bbeba0 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-analyzers.2026-10-22T10-23.md @@ -0,0 +1,9 @@ +# Final Toolchain: ANALYZE (#1002) + +Timestamp: 2026-10-22T10-23 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: +- Loop pass 3. `0 Error(s)`, `0 Warning(s)`; Time Elapsed 00:00:12.94. +- Warnings whose path is a Write Set file: none (no warning lines in the log). +- MSBuild resolved through vswhere; raw log kept under SCRATCH as `issue-1002-final3-analyze.log`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-format.2026-10-22T10-18.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-format.2026-10-22T10-18.md new file mode 100644 index 000000000..fc7df8d56 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-format.2026-10-22T10-18.md @@ -0,0 +1,42 @@ +# Final Toolchain: FORMAT (#1002) + +Timestamp: 2026-10-22T10-18 +Command: git status --porcelain --untracked-files=all; dotnet tool run csharpier format .; git status --porcelain --untracked-files=all; dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: +- Loop pass 2 (recorded clean). `csharpier format .` printed `Formatted 1674 files in 1654ms.`; `csharpier check .` exited 0 and printed `Checked 1674 files in 5104ms.` +- The two status captures are identical (51 lines each). Because every Write Set file was already modified or untracked, status alone cannot show a rewrite, so the pass also compared SHA-256 hashes of every `.cs` and `.csproj` file under UtilitiesCS, UtilitiesCS.Test, QuickFiler, and QuickFiler.Test before and after the format: 0 files changed. +- Loop pass 1 (not clean): the hash comparison found that the format rewrote 11 files, all in the Write Set (`BreadcrumbHtmlRenderer.cs`, `BreadcrumbRow.Descendants.cs`, `BreadcrumbBridgeRouter.Descendants.cs`, and the eight new test files: line wrapping only). No file outside the Write Set was rewritten, so the loop restarted at P9-T1 rather than halting. + +## Status capture (identical before and after, non-feature-folder entries) + +``` + M QuickFiler.Test/QuickFiler.Test.csproj + M QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs + M QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs + M QuickFiler/Controllers/BreadcrumbBridgeRouter.cs + M QuickFiler/Controllers/EfcFormController.Actions.cs + M QuickFiler/Controllers/EfcFormController.Breadcrumb.cs + M QuickFiler/QuickFiler.csproj + M QuickFiler/Resources/FolderBreadcrumb.js + M UtilitiesCS.Test/UtilitiesCS.Test.csproj + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs + M UtilitiesCS/UtilitiesCS.csproj +?? QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs +?? QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs +?? QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs +?? QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs +?? QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs +?? QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs +?? UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs +?? UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs +?? UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs +?? UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs +?? tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs +``` + +The remaining 25 entries of each capture are the plan file and evidence files under the feature folder. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-format.2026-10-22T10-23.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-format.2026-10-22T10-23.md new file mode 100644 index 000000000..c75bf8971 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-format.2026-10-22T10-23.md @@ -0,0 +1,42 @@ +# Final Toolchain: FORMAT (#1002) + +Timestamp: 2026-10-22T10-23 +Command: git status --porcelain --untracked-files=all; dotnet tool run csharpier format .; git status --porcelain --untracked-files=all; dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: +- Loop pass 3 (the recorded clean pass). `csharpier format .` printed `Formatted 1674 files in 1631ms.`; `csharpier check .` exited 0 and printed `Checked 1674 files in 4568ms.` +- The two status captures are identical (55 lines each). Because every Write Set file was already modified or untracked, the pass also compared SHA-256 hashes of every `.cs` and `.csproj` file under UtilitiesCS, UtilitiesCS.Test, QuickFiler, and QuickFiler.Test before and after the format: 0 files changed. +- Why pass 3: pass 1 rewrote 11 Write Set files (line wrapping in the new files) and restarted the loop; pass 2 was clean through all four steps, but P9-T5 then found two new methods below 90 percent line coverage (`SetLeafDescendants` 88.24 percent, `SelectPreferredRow` 88.24 percent: their argument-null throw and the refused-preferred-row return were untested). Two tests were added (`SetLeafDescendants_NullChildren_Throws` in BreadcrumbRowDescendantsIssue1002Tests, `BindRows_PreferredRowOutsideArchiveRoot_SelectsNothing` in BreadcrumbBridgeRouterTrashIssue1002Tests), which restarted the loop. The pass-2 artifacts (stamped 2026-10-22T10-18 to 10-21) are superseded by this pass. + +## Status capture (identical before and after; source and project entries) + +``` + M QuickFiler.Test/QuickFiler.Test.csproj + M QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs + M QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs + M QuickFiler/Controllers/BreadcrumbBridgeRouter.cs + M QuickFiler/Controllers/EfcFormController.Actions.cs + M QuickFiler/Controllers/EfcFormController.Breadcrumb.cs + M QuickFiler/QuickFiler.csproj + M QuickFiler/Resources/FolderBreadcrumb.js + M UtilitiesCS.Test/UtilitiesCS.Test.csproj + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs + M UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs + M UtilitiesCS/UtilitiesCS.csproj +?? QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs +?? QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs +?? QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs +?? QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs +?? QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs +?? QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs +?? UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs +?? UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs +?? UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs +?? UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs +?? tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs +``` + +The remaining 29 entries of each capture are the plan file and evidence files under the feature folder. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-tests.2026-10-22T10-21.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-tests.2026-10-22T10-21.md new file mode 100644 index 000000000..ac969c26c --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-tests.2026-10-22T10-21.md @@ -0,0 +1,46 @@ +# Final Toolchain: TEST-COVERAGE (#1002) + +Timestamp: 2026-10-22T10-21 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +EXIT_CODE: 0 +Output Summary: +- Loop pass 2. Test run outcome Completed: total 7577, executed 7577, passed 7577, failed 0; error 0, timeout 0, aborted 0, notExecuted 0, inconclusive 0 (summary derived from the trx; skipped 0 derived as total minus executed). +- Shell-icon carve-out: not needed; the run did not stall. +- First-party coverage summary line: `First-party coverage: lines 57523/66780 (86.14%), branches 14014/17439 (80.36%)`. +- The post-processed Cobertura and the trx were copied to SCRATCH (`issue-1002-final-cobertura.xml`, `issue-1002-final.trx`) and are not committed. + +## New tests (derived from the trx) + +| Class | Results | Methods | Passed | Not passed | +|---|---|---|---|---| +| BreadcrumbMessageCodecIssue1002Tests | 8 | 5 | 8 | 0 | +| BreadcrumbRowDescendantsIssue1002Tests | 23 | 23 | 23 | 0 | +| BreadcrumbHtmlRendererIssue1002Tests | 10 | 10 | 10 | 0 | +| BreadcrumbBridgeRouterIssue1002Tests | 11 | 11 | 11 | 0 | +| BreadcrumbBridgeRouterStaleCompletionIssue1002Tests | 7 | 7 | 7 | 0 | +| BreadcrumbBridgeRouterTrashIssue1002Tests | 6 | 6 | 6 | 0 | +| FolderBreadcrumbNestingIssue1002Tests | 4 | 4 | 4 | 0 | +| EfcFormControllerTests (Issue1002 partial): ActionDeleteAsync_WithRouter_SelectsTrashEntry, ActionDeleteAsync_WithRouter_DeliveredDocumentMarksTrashSelected, ActionDeleteAsync_AwaitedTwice_KeepsTrashSelectedAndSingleTrashRow, ActionDeleteAsync_BindFault_IsReportedAtBindBoundary | 4 | 4 | 4 | 0 | + +Every new test name from the spec Test Strategy and the plan Test Name Register appears as Passed (70 methods, 73 results). The node suite is not run by this step; see `node-suites-run`. + +## Kept-green existing classes (derived from the trx) + +| Class | Results | Failed | +|---|---|---| +| BreadcrumbHtmlRendererTests | 15 | 0 | +| BreadcrumbMessageCodecTests | 26 | 0 | +| BreadcrumbRowStateTests | 22 | 0 | +| BreadcrumbBridgeRouterTests (with .Selection partial) | 24 | 0 | +| BreadcrumbBridgeRouterQueueTests (with .Part2 partial) | 26 | 0 | +| BreadcrumbBridgeRouterScoreJoinTests | 7 | 0 | +| BreadcrumbBridgeRouterIssue439Tests (with .Activation partial) | 10 | 0 | +| BreadcrumbBridgeRouterIssue614Tests | 8 | 0 | +| BreadcrumbBridgeRouterIssue637Tests | 10 | 0 | +| BreadcrumbBridgeRouterIssue792Tests | 4 | 0 | +| BreadcrumbBridgeRouterIssue995Tests | 10 | 0 | +| EfcFormControllerTests (including ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows: Passed) | 36 | 0 | +| EfcFormControllerIssue995Tests | 4 | 0 | +| EfcHomeControllerExecuteMovesTests | 7 | 0 | +| FolderBreadcrumbAssetContractTests | 15 | 0 | +| FolderBreadcrumbRecursiveAssetIssue997Tests | 3 | 0 | diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-tests.2026-10-22T10-26.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-tests.2026-10-22T10-26.md new file mode 100644 index 000000000..a0b0103b1 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-tests.2026-10-22T10-26.md @@ -0,0 +1,46 @@ +# Final Toolchain: TEST-COVERAGE (#1002) + +Timestamp: 2026-10-22T10-26 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +EXIT_CODE: 0 +Output Summary: +- Loop pass 3 (the recorded clean pass). Test run outcome Completed: total 7579, executed 7579, passed 7579, failed 0; error 0, timeout 0, aborted 0, notExecuted 0, inconclusive 0 (summary derived from the trx; skipped 0 derived as total minus executed). +- Shell-icon carve-out: not needed; the run did not stall. +- First-party coverage summary line: `First-party coverage: lines 57523/66780 (86.14%), branches 14016/17439 (80.37%)`. +- The post-processed Cobertura and the trx were copied to SCRATCH (`issue-1002-final-cobertura.xml`, `issue-1002-final.trx`) and are not committed. + +## New tests (derived from the trx) + +| Class | Results | Methods | Passed | Not passed | +|---|---|---|---|---| +| BreadcrumbMessageCodecIssue1002Tests | 8 | 5 | 8 | 0 | +| BreadcrumbRowDescendantsIssue1002Tests | 24 | 24 | 24 | 0 | +| BreadcrumbHtmlRendererIssue1002Tests | 10 | 10 | 10 | 0 | +| BreadcrumbBridgeRouterIssue1002Tests | 11 | 11 | 11 | 0 | +| BreadcrumbBridgeRouterStaleCompletionIssue1002Tests | 7 | 7 | 7 | 0 | +| BreadcrumbBridgeRouterTrashIssue1002Tests | 7 | 7 | 7 | 0 | +| FolderBreadcrumbNestingIssue1002Tests | 4 | 4 | 4 | 0 | +| EfcFormControllerTests (Issue1002 partial): ActionDeleteAsync_WithRouter_SelectsTrashEntry, ActionDeleteAsync_WithRouter_DeliveredDocumentMarksTrashSelected, ActionDeleteAsync_AwaitedTwice_KeepsTrashSelectedAndSingleTrashRow, ActionDeleteAsync_BindFault_IsReportedAtBindBoundary | 4 | 4 | 4 | 0 | + +Every new test name from the spec Test Strategy and the plan Test Name Register appears as Passed. Two further tests added by the coverage remediation also passed: `SetLeafDescendants_NullChildren_Throws` and `BindRows_PreferredRowOutsideArchiveRoot_SelectsNothing`. The node suite is not run by this step; see `node-suites-run`. + +## Kept-green existing classes (derived from the trx) + +| Class | Results | Failed | +|---|---|---| +| BreadcrumbHtmlRendererTests | 15 | 0 | +| BreadcrumbMessageCodecTests | 26 | 0 | +| BreadcrumbRowStateTests | 22 | 0 | +| BreadcrumbBridgeRouterTests (with .Selection partial) | 24 | 0 | +| BreadcrumbBridgeRouterQueueTests (with .Part2 partial) | 26 | 0 | +| BreadcrumbBridgeRouterScoreJoinTests | 7 | 0 | +| BreadcrumbBridgeRouterIssue439Tests (with .Activation partial) | 10 | 0 | +| BreadcrumbBridgeRouterIssue614Tests | 8 | 0 | +| BreadcrumbBridgeRouterIssue637Tests | 10 | 0 | +| BreadcrumbBridgeRouterIssue792Tests | 4 | 0 | +| BreadcrumbBridgeRouterIssue995Tests | 10 | 0 | +| EfcFormControllerTests (including ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows: Passed) | 36 | 0 | +| EfcFormControllerIssue995Tests | 4 | 0 | +| EfcHomeControllerExecuteMovesTests | 7 | 0 | +| FolderBreadcrumbAssetContractTests | 15 | 0 | +| FolderBreadcrumbRecursiveAssetIssue997Tests | 3 | 0 | diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T10-19.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T10-19.md new file mode 100644 index 000000000..be684597a --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T10-19.md @@ -0,0 +1,9 @@ +# Final Toolchain: TYPECHECK (#1002) + +Timestamp: 2026-10-22T10-19 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +EXIT_CODE: 0 +Output Summary: +- Loop pass 2. `0 Error(s)`, `0 Warning(s)`; Time Elapsed 00:00:13.83. No `/p:Nullable=enable`. +- The new production partials (`BreadcrumbRow.Descendants.cs`, `BreadcrumbBridgeRouter.Descendants.cs`) carry `#nullable enable`; the `out FolderTreeNodeKey? key` value is passed to the provider with a documented null-forgiving operator, so no CS86xx diagnostic is raised. +- Raw log kept under SCRATCH as `issue-1002-final-typecheck.log`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T10-23.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T10-23.md new file mode 100644 index 000000000..29b73d9ec --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain-typecheck.2026-10-22T10-23.md @@ -0,0 +1,9 @@ +# Final Toolchain: TYPECHECK (#1002) + +Timestamp: 2026-10-22T10-23 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +EXIT_CODE: 0 +Output Summary: +- Loop pass 3. `0 Error(s)`, `0 Warning(s)`; Time Elapsed 00:00:12.59. No `/p:Nullable=enable`. +- The new production partials carry `#nullable enable`; the `out FolderTreeNodeKey? key` value reaches the provider through a documented null-forgiving operator, so no CS86xx diagnostic is raised. +- Raw log kept under SCRATCH as `issue-1002-final3-typecheck.log`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-analyzers.2026-10-22T13-32.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-analyzers.2026-10-22T13-32.md new file mode 100644 index 000000000..bc026a54e --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-analyzers.2026-10-22T13-32.md @@ -0,0 +1,7 @@ +# Remediation QA loop: analyzer build (P4-T3, #1002 cycle 1) + +Timestamp: 2026-10-22T13-32 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true +EXIT_CODE: 0 +Output Summary: +- "Build succeeded." 0 Warning(s), 0 Error(s). Time Elapsed 00:00:14.26. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-format-check.2026-10-22T13-32.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-format-check.2026-10-22T13-32.md new file mode 100644 index 000000000..4f41ec353 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-format-check.2026-10-22T13-32.md @@ -0,0 +1,7 @@ +# Remediation QA loop: format check (P4-T2, #1002 cycle 1) + +Timestamp: 2026-10-22T13-32 +Command: dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: +- Success-case line printed by the tool: "Checked 1675 files in 5318ms." diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-format.2026-10-22T13-32.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-format.2026-10-22T13-32.md new file mode 100644 index 000000000..6a93a428d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-format.2026-10-22T13-32.md @@ -0,0 +1,9 @@ +# Remediation QA loop: format (P4-T1, #1002 cycle 1) + +Timestamp: 2026-10-22T13-32 +Command: dotnet tool run csharpier format . +EXIT_CODE: 0 +Output Summary: +- "Formatted 1675 files in 1759ms." +- Observation: `git status --porcelain` before and after the formatter run is identical (no tracked change produced by the second formatter run); all modified files are files of this plan. +- Earlier formatter runs in this pass rewrote EfcFormControllerTests.Issue1002.cs layout and the modifier order `async public Task ActionOkAsync()` to `public async Task ActionOkAsync()`; both are formatter output and are retained. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-tests.2026-10-22T13-32.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-tests.2026-10-22T13-32.md new file mode 100644 index 000000000..2af07785d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-tests.2026-10-22T13-32.md @@ -0,0 +1,10 @@ +# Remediation QA loop: tests with coverage (P4-T5, #1002 cycle 1) + +Timestamp: 2026-10-22T13-32 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +EXIT_CODE: 0 +Output Summary: +- Test run outcome Completed: total 7597, executed 7597, passed 7597, failed 0 (error, timeout, aborted, notExecuted 0). +- Baseline (P0-T3) passed 7579; delta +18 = 8 (EfcFormControllerTests) + 7 (BreadcrumbMessagesIssue1002Tests) + 3 (BreadcrumbHtmlRendererIssue1002Tests). The plan's "19 (9 + 7 + 3)" figure counted 9 for Phase 1; the Phase 1 task list defines 8 tests. +- First-party coverage: lines 57541/66677 (86.30%), branches 14036/17419 (80.58%). Floors 80% line, 75% branch. +- No raw trx or Cobertura committed. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-typecheck.2026-10-22T13-32.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-typecheck.2026-10-22T13-32.md new file mode 100644 index 000000000..7c33c5282 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/remediation-typecheck.2026-10-22T13-32.md @@ -0,0 +1,7 @@ +# Remediation QA loop: type-check build (P4-T4, #1002 cycle 1) + +Timestamp: 2026-10-22T13-32 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true +EXIT_CODE: 0 +Output Summary: +- "Build succeeded." 0 Warning(s), 0 Error(s). Time Elapsed 00:00:12.49. No /p:Nullable=enable. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/scope-fences-remediation.2026-10-22T13-33.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/scope-fences-remediation.2026-10-22T13-33.md new file mode 100644 index 000000000..2a00c5312 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/scope-fences-remediation.2026-10-22T13-33.md @@ -0,0 +1,11 @@ +# Scope fences, remediation cycle 1 (P4-T13, #1002) + +Timestamp: 2026-10-22T13-33 +Command: git diff --name-only ; git status --porcelain; git diff -U0 -- '*.cs' (added ExcludeFromCodeCoverage lines); git diff --stat -- coverage.config +EXIT_CODE: 0 +Output Summary: +- Cycle-1 working changes (git status --porcelain, uncommitted): modified QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs, QuickFiler/Controllers/EfcFormController.Actions.cs, QuickFiler/Controllers/EfcFormController.Breadcrumb.cs, UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs, UtilitiesCS.Test/UtilitiesCS.Test.csproj, UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs, the remediation plan; untracked UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs and this cycle's evidence files. None is on the spec's excluded list; none is under .claude/rules/. +- The merge-base `git diff --name-only` also lists files committed earlier on the branch by the original change (including the router, renderer and codec files and .claude/agent-memory notes); those are outside this cycle's edits and were not touched by it. +- Added ExcludeFromCodeCoverage lines in the merge-base diff over *.cs: 5, located on ActionOkAsync, ActionCancelAsync, CreateFolderAsync, RefreshSuggestionsAsync (P1-T10 recorded YES) and ConfigureBreadcrumbControl. No other member. +- `git diff --stat` of coverage.config against the merge base: empty (no change). +- No .trx, .coverage or Cobertura document appears in git status. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/scope-fences.2026-10-22T10-15.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/scope-fences.2026-10-22T10-15.md new file mode 100644 index 000000000..910e2f959 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/scope-fences.2026-10-22T10-15.md @@ -0,0 +1,21 @@ +# QA Gate: Scope Fences (#1002) + +Timestamp: 2026-10-22T10-15 +Command: MB=$(git merge-base origin/main HEAD); git diff --name-status $MB; git status --porcelain --untracked-files=all; grep -cE "FolderBreadcrumbBridgeRouter|FolderBreadcrumbDocument" QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs QuickFiler/Controllers/EfcFormController*.cs; grep -cE "FolderBreadcrumbBridgeRouter|FolderBreadcrumbDocument" QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs; git diff $MB -- 'QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs' UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs | grep '^+' | grep -cE 'ArrowLeft|ArrowRight|ArrowUp|ArrowDown|HandleArrowKeyAsync'; grep -cE (same tokens) QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs +EXIT_CODE: 0 +Output Summary: +- Code paths changed (merge-base diff plus porcelain status): only Write Set files. Modified: `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs`, `.Arrows.cs`, `.Selection.cs`, `QuickFiler/Controllers/EfcFormController.Actions.cs`, `EfcFormController.Breadcrumb.cs`, `QuickFiler/Resources/FolderBreadcrumb.js`, `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs`, `BreadcrumbMessages.cs`, `BreadcrumbMessageCodec.cs`, `BreadcrumbHtmlRenderer.cs`, `BreadcrumbDocumentAssets.cs`, and the four project files. Added (untracked): `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs`, `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs`, the eight new test files, and `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs`. All other changed paths are under `/`. +- Project files in the change: exactly the four in the Write Set (`QuickFiler/QuickFiler.csproj`, `QuickFiler.Test/QuickFiler.Test.csproj`, `UtilitiesCS/UtilitiesCS.csproj`, `UtilitiesCS.Test/UtilitiesCS.Test.csproj`). +- Excluded files (BreadcrumbDescendantNode.cs, every FolderBreadcrumbBridgeRouter file, every BreadcrumbSelectionSession file, BreadcrumbRenderProjection.cs, every BreadcrumbBridgeCoordinator file, ItemViewer.Breadcrumb.cs, QfcItemController.BreadcrumbWiring.cs, FolderBreadcrumb.html, Resources.resx, EfcDataModel.cs) in the diff or status: 0. +- QFC reference search over the EFC router and controller files: 0. Positive control (`QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs`): 3. +- Added lines containing `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown`, or `HandleArrowKeyAsync` in the router files and `BreadcrumbDocumentAssets.cs`: 0 (tracked diff) and 0 (new untracked `BreadcrumbBridgeRouter.Descendants.cs`). + +## Residue outside the Write Set and the feature folder + +The merge-base diff also lists three paths committed on this branch before execution began (by the promotion and research commits), not by this plan's tasks: + +- `.claude/agent-memory/task-researcher/index_quickfiler_efc_defects.md` (modified) +- `.claude/agent-memory/task-researcher/project_efc_qfc_separate_breadcrumb_surfaces_1002.md` (added) +- `docs/features/potential/promoted/2026-10-22-efc-qfc-folder-tree-selection-and-nesting.md` (added; promotion lifecycle record) + +These are agent-memory and promotion-lifecycle records owned by the orchestrator, not source files, and none is a spec-excluded file. The scope check therefore holds for every path this execution changed; the three inherited paths are reported here rather than reverted, because reverting committed lifecycle records is outside the plan and the executor does not commit. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T10-29.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T10-29.md new file mode 100644 index 000000000..c5563ed70 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T10-29.md @@ -0,0 +1,70 @@ +# Self-Review: Policy Audit, Code Review, and Feature Audit Checklists (#1002) + +Timestamp: 2026-10-22T10-29 +Command: review of `git diff $(git merge-base origin/main HEAD)` plus the untracked new files (`git status --porcelain --untracked-files=all`), against the checklists below +EXIT_CODE: 0 +Output Summary: no FAIL row. Code and test diff: 15 tracked files changed (308 insertions, 43 deletions) plus 2 new production files, 8 new test files, and 1 new node suite. + +Template resolution (deviation): `docs/features/templates/policy_audit/` does not exist in TaskMaster, and the template resolver named by `.claude/skills/policy-audit-template-usage/SKILL.md` (`mcp__drm-copilot__resolve_policy_audit_template_asset`) is not among the executor's tools. The policy-audit checklist below therefore uses that skill's canonical section list; the code-review and feature-audit checklists use the review dimensions those artifacts carry (correctness, design, error handling, tests, scope; acceptance criteria and evidence). The formal review artifacts remain the feature reviewer's to produce. + +## Policy audit (canonical sections) + +| # | Item | Result | Evidence | +|---|---|---|---| +| PA-1 | Executive summary: three defects fixed with fail-before and pass-after evidence | PASS | `efc-descendants-*`, `efc-trash-*`, `qfc-nesting-*` under regression-testing | +| PA-2 | General unit test policy: independent, isolated, fast, deterministic, readable | PASS | `csharp-isolation` (each class in a fresh process plus all in one process, 0 failures); TaskCompletionSource barriers only | +| PA-3 | General unit test policy: no temporary files, no external services, mocks for boundaries | PASS | `test-policy-audit` (0 banned-token matches; strict Moq mocks) | +| PA-4 | General code change policy: simplicity, separation of concerns, files under 500 lines | PASS | `file-sizes` (largest changed production file 471; largest new test file 487) | +| PA-5 | General code change policy: fail fast, project logging pattern | PASS | codec fails fast through `Fail`; router rejections use the existing `log.Error` pattern; provider faults logged at the I/O boundary | +| PA-6 | C# code change policy: CSharpier, analyzers, nullable | PASS | `final-toolchain-format`, `final-toolchain-analyzers`, `final-toolchain-typecheck` (loop pass 3, all exit 0, 0 warnings) | +| PA-7 | C# code change policy: `#nullable enable` on new production files; explicit Compile Include | PASS | both new production partials carry the directive; four project files register every new file | +| PA-8 | C# unit test policy: MSTest, Moq, FluentAssertions | PASS | `test-policy-audit` | +| PA-9 | `#nullable enable` on new test files | N/A (partial) | UtilitiesCS.Test files carry it; QuickFiler.Test compiles as C# 7.3, where the directive is a compile error, and none of its files carries it (spec: consistent with neighbors). Recorded in `efc-descendants-fail-before`. | +| PA-10 | Test coverage detail: new files and methods at least 90 percent, changed lines not reduced | PASS | `coverage-comparison` (all new files and methods 100 percent; changed lines 100 percent) | +| PA-11 | Repository coverage floors 80 line and 75 branch, not lowered | PASS | 86.10 to 86.14 percent line; 80.31 to 80.37 percent branch | +| PA-12 | Test execution metrics recorded from the trx | PASS | `final-toolchain-tests` (7579 passed, 0 failed) | +| PA-13 | Code quality checks: one clean four-step pass in CLAUDE.md order | PASS | loop pass 3 artifacts stamped 2026-10-22T10-23 and 10-26 | +| PA-14 | Gaps and exceptions documented | PASS | this file (Observations), `scope-fences` residue, `coverage-comparison` observation | +| PA-15 | No coverage exclusion added | PASS | `coverage-comparison` (coverage.config unchanged; 0 ExcludeFromCodeCoverage added) | +| PA-16 | Policy documents under `.claude/rules/` not modified | PASS | not in the diff | + +## Code review + +| # | Item | Result | Evidence | +|---|---|---|---| +| CR-1 | Descendant identity validated against the addressed row's own tree before any provider call or state change (I1) | PASS | `TryCollapseDescendant` / `TryBeginDescendantExpand` / `GetDescendantPath` search only the row's nodes; tests `DescendantRequest_UnknownNode_IsRejectedWithoutStateChange`, `DescendantRequest_NodeFromAnotherRow_IsRejected` | +| CR-2 | Stale completion: same row instance and request generation checked after every await (I2), including first-level `ExpandLeafAsync` | PASS | `BreadcrumbBridgeRouter.Descendants.cs`, `ExpandLeafAsync`; the seven stale-completion tests | +| CR-3 | Toggle never changes selection (I3) | PASS | `DescendantToggle_DoesNotChangeSelection` | +| CR-4 | Activation commits only through `SelectHierarchyPath`; rejection leaves selection and highlight unchanged (I4) | PASS | highlight applied immediately before `CommitSelection`; `DescendantActivate_OutsideArchiveRoot_LeavesSelectionAndHighlightUnchanged` | +| CR-5 | Trash selection is inside the delivered document, with no follow-up render (I5) | PASS | `SelectPreferredRow` runs before `DeliverDocument`; `BindRows_PreferredTrashRow_PostsNoFollowUpRender` | +| CR-6 | Thread affinity: no lock and no `ConfigureAwait(false)` in router files (I6) | PASS | `thread-affinity` | +| CR-7 | `SelectRow` value computation shared, not duplicated | PASS | `TryGetRowSelection` used by `SelectRow` and `SelectPreferredRow` | +| CR-8 | Async-void boundary unchanged: handlers log and do not throw new exception types | PASS | `OnHostMessageReceived` unchanged; provider faults caught in the handler | +| CR-9 | Delete-gesture faults still reported at the `BindBreadcrumbRowsAsync` boundary; null-router guard preserved | PASS | `ActionDeleteAsync_BindFault_IsReportedAtBindBoundary`, `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows` | +| CR-10 | Rendering: names and paths HTML-encoded; pinned first-level prefix kept; key-less rows unchanged | PASS | `RenderRowFragment_DescendantNames_AreHtmlEncoded`, `RenderRowFragment_KeylessChildren_RenderUnchanged`, BreadcrumbHtmlRendererTests green | +| CR-11 | Bridge script branch order (node affordance, keyed child, key-less child, leaf affordance; Enter/Space before row confirmation) | PASS | the three `BridgeJs_` tests | +| CR-12 | `BreadcrumbDescendantNode` reused unchanged; QFC router files untouched | PASS | `scope-fences` | +| CR-13 | Public API additive and source-compatible (optional parameters, new members only) | PASS | every existing caller compiled unchanged (`seam-build-*`) | +| CR-14 | Unused or dead code introduced | PASS | none; `BindFolderRows` is still used by `BindSourceFolderRows` | + +## Feature audit + +| # | Item | Result | Evidence | +|---|---|---|---| +| FA-1 | AC1 to AC26 each traced to a task and evidence and checked off | PASS | `ac-reconciliation` (26 of 26) | +| FA-2 | Group A fail-before set failed before and passed after | PASS | `efc-descendants-fail-before`, `efc-descendants-pass-after` | +| FA-3 | Group B fail-before set failed before and passed after | PASS | `efc-trash-fail-before`, `efc-trash-pass-after` | +| FA-4 | Group C fail-before set failed before and passed after (C# and node) | PASS | `qfc-nesting-fail-before`, `qfc-nesting-pass-after` | +| FA-5 | Node suites pass in normal, reversed, and seeded shuffled order | PASS | `node-suites-run` (seed 1002; the shuffle coincided with the reversed order and is recorded as such) | +| FA-6 | Scope fences: no excluded file in the diff | PASS | `scope-fences`; the three orchestrator-owned paths committed before execution are recorded there | +| FA-7 | Evidence committed as Markdown projections without host paths | PASS | `evidence-form` | +| FA-8 | Manual Outlook check recorded as pending and not claimed as executed | PASS | `manual-outlook-check` | +| FA-9 | Existing tests kept green without edits to their assertions | PASS | `final-toolchain-tests` kept-green table; no existing test file is in the diff | + +## Observations (not FAIL rows) + +1. `EfcFormController.Breadcrumb.cs` whole-file line coverage fell from 67.83 to 64.46 percent, because the unchanged private `BindFolderRows` lost its only covered caller when the delete gesture moved to the awaited bind (P5-T2). Its remaining caller, `BindSourceFolderRows`, is untested at baseline and now. A reviewer may want a test for `BindSourceFolderRows`. +2. Two tests outside the Test Name Register were added during the Phase 9 loop to bring `SetLeafDescendants` and `SelectPreferredRow` to 100 percent: `SetLeafDescendants_NullChildren_Throws` and `BindRows_PreferredRowOutsideArchiveRoot_SelectsNothing`. +3. The P8-T5 isolation runs used the class set before those two tests were added; both tests passed in the final full run. +4. One Group A test expectation was corrected during P3-T10 (grandchild depth attribute 1, not 2), recorded in `efc-descendants-pass-after`. +5. `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` is 487 lines; new tests belong in a new file. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T13-34.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T13-34.md new file mode 100644 index 000000000..bfd9fb3aa --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T13-34.md @@ -0,0 +1,48 @@ +# Self-Review, remediation cycle 1 (#1002) + +Timestamp: 2026-10-22T13-34 +Command: review of the branch diff from the merge base of origin/main and HEAD plus the untracked cycle-1 files (git status --porcelain), against the checklists below +EXIT_CODE: 0 +Output Summary: no FAIL row. Cycle-1 edits: 6 modified tracked source files, 1 new test file, evidence files. + +Template resolution (deviation): `docs/features/templates/policy_audit/` does not exist in TaskMaster and the template resolver tool is not among the executor's tools, so the checklists follow the first-cycle self-review (`self-review.2026-10-22T10-29.md`): the policy-audit canonical sections, the code-review dimensions and the feature-audit items, restricted to the cycle-1 findings. + +## Policy audit + +| # | Item | Result | Evidence | +|---|---|---|---| +| PA-1 | Unit test policy: independent, isolated, deterministic, no sleeps or delays | PASS | new tests use strict mocks and a TaskCompletionSource; full parallel-default run 7597 passed, 0 failed (remediation-tests) | +| PA-2 | No temporary files, no external services | PASS | tests build in-memory mocks only | +| PA-3 | Files at or under 500 lines | PASS | file-sizes-remediation (largest 343) | +| PA-4 | CSharpier, analyzers, nullable, tests, in order, single clean pass | PASS | remediation-format, -format-check, -analyzers, -typecheck, -tests (all exit 0, 0 warnings) | +| PA-5 | Coverage floors 80 line / 75 branch not lowered; changed files at or above floor | PASS | coverage-comparison (86.30% / 80.58%) | +| PA-6 | New tests target full coverage of new code | PASS | b1/b2 verification; 8+7+3 new tests pass | +| PA-7 | Exclusions limited to ratified members; none whole-file; no coverage.config change | PASS | scope-fences-remediation (5 attributes on the named members) | +| PA-8 | `.claude/rules/` and `.github/instructions/` unmodified; no raw trx or Cobertura in git | PASS | git status | +| PA-9 | `#nullable enable` on the new UtilitiesCS.Test file; none on C# 7.3 QuickFiler.Test file | PASS | BreadcrumbMessagesIssue1002Tests.cs; EfcFormControllerTests.Issue1002.cs | + +## Code review + +| # | Item | Result | Evidence | +|---|---|---|---| +| CR-1 | `.child` flex container keeps layout; ellipsis moved to `.child .name`; flex test unchanged | PASS | BaseCss_ChildName_CarriesEllipsisRule, BaseCss_Child_IsFlexRow | +| CR-2 | Keyless leaf-child and subfolderResult emitters render bare text without `.name` (observation, out of scope) | N/A | child-name-markup-check | +| CR-3 | Selected descendant highlight via attribute selector, no new class (AC9 preserved) | PASS | Light/DarkThemeCss tests, b4-grep | +| CR-4 | Null-viewer arm of ApplyDeleteGestureAsync tested | PASS | ApplyDeleteGestureAsync_NullViewer_RetainsTrashRowAndDoesNotBind | +| CR-5 | Production change limited to attributes and CSS constants | PASS | diff of QuickFiler/Controllers shows attributes, comments and using directives only (plus formatter modifier reorder on ActionOkAsync) | +| CR-6 | Attribute form compiles on net48 | PASS | exclude-attribute-form (COMMENT form, CS0246 for Justification) | + +## Findings status + +| Finding | Status | Evidence | +|---|---|---| +| B-1 | PASS: Breadcrumb.cs 93/101 lines, 34/42 branches (thresholds 84 and 24) | b1-per-file-coverage | +| B-2 | PASS: BreadcrumbMessages.cs seven guard lines 2/2, 14/14 branches | b2-per-file-coverage | +| B-3 | PASS: exemption ratified by the maintainer, member-level attributes; Actions.cs 92.86% / 81.25%, Breadcrumb.cs 92.08% / 80.95% | b3-per-file-coverage | +| B-4 | PASS: 2 aria-selected rules | b4-grep | +| CR-1 | PASS | css-highlight-pass-after | +| CR-4 | PASS | efc-bind-tests-pass | + +## Observations + +- The plan states 9 expected Phase 1 tests and 19 new tests; the Phase 1 task list defines 8 tests (P1-T6 is two methods), so 8 + 7 + 3 = 18 new tests and the pass count rose from 7579 to 7597. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T14-07.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T14-07.md new file mode 100644 index 000000000..a7b277394 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/self-review.2026-10-22T14-07.md @@ -0,0 +1,42 @@ +# Self-Review, remediation cycle 2 (#1002) + +Timestamp: 2026-10-22T14-07 +Command: review of the cycle-2 diff (git diff HEAD, git status --porcelain) and the branch diff from the merge base of origin/main and HEAD against the checklists below +EXIT_CODE: 0 +Output Summary: no FAIL row. Cycle-2 edits: 4 modified source and test files, spec.md, the plan, cycle2 evidence files. + +Template resolution (deviation): docs/features/templates/policy_audit/ does not exist in TaskMaster, so the checklists follow the cycle-1 self-review (self-review.2026-10-22T13-34.md): the policy-audit canonical sections, the code-review dimensions and the feature-audit items, restricted to the cycle-2 findings. + +## Policy audit + +| # | Item | Result | Evidence | +|---|---|---|---| +| PA-1 | Unit tests independent, isolated, deterministic, no sleeps or delays | PASS | the new tests build their own strict mocks; P3-T1 observes the fault synchronously; full parallel-default run 7599 passed, 0 failed (cycle2-tests) | +| PA-2 | No temporary files, no external services | PASS | in-memory mocks and constant text only | +| PA-3 | Files at or under 500 lines | PASS | cycle2-file-sizes (largest 368) | +| PA-4 | CSharpier, analyzers, nullable, tests, in order, single clean pass | PASS | cycle2-format, -format-check, -analyzers, -typecheck, -tests (all exit 0, 0 warnings) | +| PA-5 | Coverage floors not lowered; changed files at or above floor | PASS | cycle2-coverage-comparison (86.31% line, 80.59% branch; Actions.cs 92.86% / 81.25%; BreadcrumbDocumentAssets.cs has no instrumented line) | +| PA-6 | New tests cover the new behavior, fail-before observed | PASS | cycle2-delivered-wait-fail-before, cycle2-child-name-fail-before | +| PA-7 | Exclusions limited to the five ratified members; no coverage.config change | PASS | cycle2-scope-fences (5 attributes, empty coverage.config stat) | +| PA-8 | .claude/rules and .github/instructions unmodified; no raw trx or Cobertura in git | PASS | git status | +| PA-9 | No #nullable enable on the C# 7.3 QuickFiler.Test file | PASS | EfcFormControllerTests.Issue1002.cs unchanged header | + +## Findings status + +| Finding | Status | Evidence | +|---|---|---| +| R-1 | PASS: AC23 clause replaced, criterion still checked, Planner Amendments section appended, AC22 and AC24 unchanged, attribute count 5, empty coverage.config stat | cycle2-r1-verification, cycle2-r1-final | +| N-1 | PASS: comment above ActionCancelAsync names the EfcViewer UiSyncContext and Close; MessageBox count 5 to 4; attribute and body untouched | cycle2-n1-verification | +| N-3 | PASS: BoundaryErrorSink wired to TrySetException on the delivered source; new test failed before and passes after; 45 of 45 pass | cycle2-delivered-wait-fail-before, -pass-after | +| N-5 | PASS: subfolderResult handler wraps the display name in span.name; new test failed before and passes after; 29 of 29 renderer tests pass | cycle2-child-name-fail-before, -pass-after, -dispositions | + +## Coordinator ruling (AC9) + +| Item | Result | Evidence | +|---|---|---| +| AC9 stays authoritative and true: the key-less branch of BreadcrumbHtmlRenderer was deliberately left unedited because it has no production caller; the retained subfolderResult name-span wrap is the client-side transient render in BridgeJs, which AC9 (server-side key-less render) does not govern | PASS | git diff HEAD --stat of BreadcrumbHtmlRenderer.cs is empty; RenderRowFragment_KeylessChildren_RenderUnchanged passes unchanged | + +## Observations + +- The plan P4-T5 asserts an empty diff of BreadcrumbHtmlRenderer.cs against the merge base; that diff is non-empty because cycles 0 and 1 already changed the file, so the check was made against HEAD (no cycle-2 edit). +- BreadcrumbDocumentAssets.cs is not in the Cobertura document (string constants only), so its per-file floor is not evaluable. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/test-policy-audit.2026-10-22T10-16.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/test-policy-audit.2026-10-22T10-16.md new file mode 100644 index 000000000..64349501d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/test-policy-audit.2026-10-22T10-16.md @@ -0,0 +1,23 @@ +# QA Gate: Test Policy Audit (#1002) + +Timestamp: 2026-10-22T10-16 +Command: per new test file: grep -c for `using Microsoft.VisualStudio.TestTools.UnitTesting;`, `using Moq;`, `using FluentAssertions;`, `new Mock<`, `new Mock<...>(MockBehavior.Strict)`, the `// Arrange` / `// Act` / `// Assert` markers, `-E "Path\.GetTemp|File\.Write|File\.Create|Thread\.Sleep|Task\.Delay|new WebView2BreadcrumbHost"`, and the file name in its project file +EXIT_CODE: 0 +Output Summary: all eight rows pass. Every mock constructed in the new tests is `MockBehavior.Strict` (six constructions, all strict); no file matches a banned token; every file has its `` entry. Files that construct no mock carry no `using Moq;`: the three UtilitiesCS.Test files and the nesting contract exercise pure types with no collaborator to mock, and the stale-completion and trash router classes obtain their strict mocks from the shared `EfcDescendantRouterFixture` (declared in `BreadcrumbBridgeRouterIssue1002Tests.cs`); the stale-completion file adds sequence setups on that fixture and therefore carries `using Moq;`. + +| File | MSTest | Moq | FluentAssertions | Mocks (strict/total) | AAA | Banned tokens | Compile Include | Result | +|---|---|---|---|---|---|---|---|---| +| UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs | yes | n/a (no collaborator) | yes | 0/0 | yes | 0 | yes | PASS | +| UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs | yes | n/a (no collaborator) | yes | 0/0 | yes | 0 | yes | PASS | +| UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs | yes | n/a (no collaborator) | yes | 0/0 | yes | 0 | yes | PASS | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs | yes | yes | yes | 2/2 (fixture provider and host) | yes | 0 | yes | PASS | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs | yes | yes (fixture setups) | yes | 0/0 own; shared strict fixture | yes | 0 | yes | PASS | +| QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs | yes | via shared strict fixture | yes | 0/0 own; shared strict fixture | yes | 0 | yes | PASS | +| QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs | yes | yes | yes | 4/4 | yes | 0 | yes | PASS | +| QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs | yes | n/a (text contract) | yes | 0/0 | yes | 0 | yes | PASS | + +Additional checks: + +- No test creates a file, sleeps, or delays; ordering in the stale-completion class uses `TaskCompletionSource` barriers. +- No test constructs a real `WebView2BreadcrumbHost`; all host access goes through strict `IBreadcrumbWebHost` mocks. +- Tests share no static mutable state: each test builds a fresh fixture in `[TestInitialize]` or a local factory. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/thread-affinity.2026-10-22T10-15.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/thread-affinity.2026-10-22T10-15.md new file mode 100644 index 000000000..ebdf65a6e --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/thread-affinity.2026-10-22T10-15.md @@ -0,0 +1,12 @@ +# QA Gate: EFC Router Thread Affinity (#1002) + +Timestamp: 2026-10-22T10-15 +Command: cat QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs | grep -c "ConfigureAwait(false)"; cat QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs | grep -cE "lock \("; grep -c "ConfigureAwait" QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs; git diff $(git merge-base origin/main HEAD) -- QuickFiler/Controllers/EfcFormController.Actions.cs | grep '^+' | grep -c "ConfigureAwait(false)"; grep -c "ConfigureAwait(false)" UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.Descendants.cs +EXIT_CODE: 0 +Output Summary: +- Files searched: `BreadcrumbBridgeRouter.cs`, `.Arrows.cs`, `.Selection.cs`, `.Descendants.cs` (all four files matching `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs`). +- `ConfigureAwait(false)` in those files: 0. +- `lock (` in those files: 0. +- `ConfigureAwait` anywhere in `BreadcrumbBridgeRouter.Descendants.cs`: 0. (A first run found 1 match in the class XML comment, which named the token; the comment was reworded and the search re-run.) +- Added lines containing `ConfigureAwait(false)` in the merge-base diff of `QuickFiler/Controllers/EfcFormController.Actions.cs`: 0. +- Positive control: `ConfigureAwait(false)` in `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.Descendants.cs` (the QFC router): 5. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/breadcrumb-messages-null-guards-pass.2026-10-22T13-26.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/breadcrumb-messages-null-guards-pass.2026-10-22T13-26.md new file mode 100644 index 000000000..b3f75f0be --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/breadcrumb-messages-null-guards-pass.2026-10-22T13-26.md @@ -0,0 +1,10 @@ +# BreadcrumbMessages null-guard tests pass (P2-T5, #1002 cycle 1) + +Timestamp: 2026-10-22T13-26 +Command: dotnet tool run csharpier format . ; msbuild UtilitiesCS.Test/UtilitiesCS.Test.csproj /t:Build; vstest.console.exe UtilitiesCS.Test.dll /InIsolation /TestCaseFilter:FullyQualifiedName~BreadcrumbMessagesIssue1002Tests ; dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: +- Scoped run: total 7, executed 7, passed 7, failed 0. +- Tests: BreadcrumbInboundMessage_NullType/NullRowId, BreadcrumbOutboundMessage_NullType, BreadcrumbRenderMessage_NullHtml, BreadcrumbSubfolderResultMessage_NullRequestId/NullRowId/NullChildren; each asserts ArgumentNullException with the expected ParamName. +- `git grep -n -e BreadcrumbMessagesIssue1002Tests -- UtilitiesCS.Test/UtilitiesCS.Test.csproj` returns exactly one hit (the Compile Include). +- `dotnet tool run csharpier check .`: "Checked 1675 files in 4820ms.", exit 0. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/csharp-isolation.2026-10-22T10-17.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/csharp-isolation.2026-10-22T10-17.md new file mode 100644 index 000000000..99e92fa4c --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/csharp-isolation.2026-10-22T10-17.md @@ -0,0 +1,20 @@ +# C# Test Isolation: New Classes in Fresh Processes (#1002) + +Timestamp: 2026-10-22T10-17 +Command: for each class in reverse spec Test Strategy order: vstest.console.exe /InIsolation /TestCaseFilter:"FullyQualifiedName~.." /ResultsDirectory:SCRATCH/results-issue-1002-iso- /Logger:"trx;LogFileName=issue-1002-iso-.trx"; then one vstest.console.exe process over both assemblies with the eight class clauses joined by `|` (launcher SCRATCH/issue-1002-isolation.ps1) +EXIT_CODE: 0 +Output Summary: zero failures in every run; counts derived from each trx. + +| Run | Class (fresh process) | Assembly | Exit | Total / passed / failed | +|---|---|---|---|---| +| 1 | FolderBreadcrumbNestingIssue1002Tests | QuickFiler.Test | 0 | 4 / 4 / 0 | +| 2 | EfcFormControllerTests (holds the EfcFormControllerTests.Issue1002.cs partial) | QuickFiler.Test | 0 | 36 / 36 / 0 | +| 3 | BreadcrumbBridgeRouterTrashIssue1002Tests | QuickFiler.Test | 0 | 6 / 6 / 0 | +| 4 | BreadcrumbBridgeRouterStaleCompletionIssue1002Tests | QuickFiler.Test | 0 | 7 / 7 / 0 | +| 5 | BreadcrumbBridgeRouterIssue1002Tests | QuickFiler.Test | 0 | 11 / 11 / 0 | +| 6 | BreadcrumbHtmlRendererIssue1002Tests | UtilitiesCS.Test | 0 | 10 / 10 / 0 | +| 7 | BreadcrumbRowDescendantsIssue1002Tests | UtilitiesCS.Test | 0 | 23 / 23 / 0 | +| 8 | BreadcrumbMessageCodecIssue1002Tests | UtilitiesCS.Test | 0 | 8 / 8 / 0 | +| all | all eight classes in one process | both | 0 | 105 / 105 / 0 | + +Notes: the codec class reports 8 results because three methods carry two data rows each. The EfcFormControllerTests class runs its existing partials with the new partial, since a class filter cannot separate partial files. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/css-highlight-fail-before.2026-10-22T13-27.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/css-highlight-fail-before.2026-10-22T13-27.md new file mode 100644 index 000000000..ec2eaeee5 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/css-highlight-fail-before.2026-10-22T13-27.md @@ -0,0 +1,9 @@ +# CSS highlight fail-before (P3-T4, expect-fail, #1002 cycle 1) + +Timestamp: 2026-10-22T13-27 +Command: vstest.console.exe UtilitiesCS.Test.dll /InIsolation /TestCaseFilter:FullyQualifiedName~BreadcrumbHtmlRendererIssue1002Tests (before the BreadcrumbDocumentAssets.cs change) +EXIT_CODE: 1 +Output Summary: +- total 13, executed 13, passed 10, failed 3 (expected failure). +- Failing: LightThemeCss_SelectedChild_UsesSelectionBackground, DarkThemeCss_SelectedChild_UsesSelectionBackground (the theme blocks hold no `.child[aria-selected]` rule), BaseCss_ChildName_CarriesEllipsisRule (no `.child .name {` rule in BaseCss). +- Passing: the 10 pre-existing tests of the class, including BaseCss_Child_IsFlexRow. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/css-highlight-pass-after.2026-10-22T13-28.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/css-highlight-pass-after.2026-10-22T13-28.md new file mode 100644 index 000000000..6c8d0a725 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/css-highlight-pass-after.2026-10-22T13-28.md @@ -0,0 +1,9 @@ +# CSS highlight pass-after (P3-T8, #1002 cycle 1) + +Timestamp: 2026-10-22T13-28 +Command: vstest.console.exe UtilitiesCS.Test.dll /InIsolation /TestCaseFilter:FullyQualifiedName~BreadcrumbHtmlRendererIssue1002Tests (after the BreadcrumbDocumentAssets.cs change) +EXIT_CODE: 0 +Output Summary: +- total 13, executed 13, passed 13, failed 0 (10 pre-existing plus the 3 new tests). +- `git grep -c -e aria-selected -- UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` reports 2 (light and dark rules). +- BaseCss_Child_IsFlexRow passes unchanged. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-child-name-fail-before.2026-10-22T14-00.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-child-name-fail-before.2026-10-22T14-00.md new file mode 100644 index 000000000..4f16b9fdf --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-child-name-fail-before.2026-10-22T14-00.md @@ -0,0 +1,9 @@ +# Cycle 2 P4-T2: child-name fail-before (expect-fail) + +Timestamp: 2026-10-22T14-00 +Command: vstest.console.exe UtilitiesCS.Test.dll /InIsolation /TestCaseFilter:FullyQualifiedName~BreadcrumbHtmlRendererIssue1002Tests (before the BreadcrumbDocumentAssets.cs change) +EXIT_CODE: 1 +Output Summary: +- total 14, executed 14, passed 13, failed 1 (expected failure; assertion failure). +- Failing: BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan (the handler text does not contain the name span statements). +- The other 13 tests of the class pass, including RenderRowFragment_KeylessChildren_RenderUnchanged. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-child-name-pass-after.2026-10-22T14-01.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-child-name-pass-after.2026-10-22T14-01.md new file mode 100644 index 000000000..952ca212e --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-child-name-pass-after.2026-10-22T14-01.md @@ -0,0 +1,10 @@ +# Cycle 2 P4-T4: child-name pass-after + +Timestamp: 2026-10-22T14-01 +Command: dotnet tool run csharpier format . ; msbuild UtilitiesCS.Test/UtilitiesCS.Test.csproj /t:Build (Debug, AnyCPU); vstest.console.exe UtilitiesCS.Test.dll /InIsolation /TestCaseFilter:FullyQualifiedName~BreadcrumbHtmlRendererIssue1002Tests|FullyQualifiedName~BreadcrumbHtmlRendererTests +EXIT_CODE: 0 +Output Summary: +- Formatter printed "Formatted 1675 files in 1649ms."; git status --porcelain before and after listed the same four modified source and test files (EfcFormController.Actions.cs, EfcFormControllerTests.Issue1002.cs, BreadcrumbHtmlRendererIssue1002Tests.cs, BreadcrumbDocumentAssets.cs). +- Project build: 0 errors. +- Scoped run of both classes: total 29, executed 29, passed 29, failed 0 (pre-existing 28 plus the 1 new BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan). +- RenderRowFragment_KeylessChildren_RenderUnchanged passes with its assertions unchanged. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-delivered-wait-fail-before.2026-10-22T13-58.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-delivered-wait-fail-before.2026-10-22T13-58.md new file mode 100644 index 000000000..6619fe461 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-delivered-wait-fail-before.2026-10-22T13-58.md @@ -0,0 +1,9 @@ +# Cycle 2 P3-T2: delivered-wait fail-before (expect-fail) + +Timestamp: 2026-10-22T13-58 +Command: vstest.console.exe QuickFiler.Test.dll /InIsolation /TestCaseFilter:FullyQualifiedName~EfcFormControllerTests (assembly path excludes the .claude worktree tree) before the CreateIssue1002Controller change +EXIT_CODE: 1 +Output Summary: +- total 45, executed 45, passed 44, failed 1 (expected failure; assertion failure, no hang). +- Failing: BindFolderRows_FaultBeforeDelivery_FaultsDeliveredTaskPromptly ("Expected delivered.Task.IsFaulted to be True because a pre-delivery fault must fail the wait instead of leaving it pending, but found False."). +- The 44 pre-existing EfcFormControllerTests tests pass. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-delivered-wait-pass-after.2026-10-22T13-59.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-delivered-wait-pass-after.2026-10-22T13-59.md new file mode 100644 index 000000000..d04d86219 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/cycle2-delivered-wait-pass-after.2026-10-22T13-59.md @@ -0,0 +1,9 @@ +# Cycle 2 P3-T4: delivered-wait pass-after + +Timestamp: 2026-10-22T13-59 +Command: dotnet tool run csharpier format . ; msbuild QuickFiler.Test/QuickFiler.Test.csproj /t:Build (Configuration Debug, Platform AnyCPU); vstest.console.exe QuickFiler.Test.dll /InIsolation /TestCaseFilter:FullyQualifiedName~EfcFormControllerTests +EXIT_CODE: 0 +Output Summary: +- Formatter printed "Formatted 1675 files in 2718ms."; git status --porcelain before and after the formatter listed the same modified tracked files (EfcFormController.Actions.cs, EfcFormControllerTests.Issue1002.cs, spec.md, the plan), so it changed no file beyond this plan's file set. +- Project build: 0 warnings, 0 errors. +- Scoped run: total 45, executed 45, passed 45, failed 0. The new test BindFolderRows_FaultBeforeDelivery_FaultsDeliveredTaskPromptly and all 44 pre-existing tests pass. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-bind-tests-pass.2026-10-22T13-22.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-bind-tests-pass.2026-10-22T13-22.md new file mode 100644 index 000000000..01b709a7a --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-bind-tests-pass.2026-10-22T13-22.md @@ -0,0 +1,11 @@ +# EFC bind-path tests pass (P1-T8, #1002 cycle 1) + +Timestamp: 2026-10-22T13-22 +Command: dotnet tool run csharpier format . ; msbuild QuickFiler.Test/QuickFiler.Test.csproj /t:Build; vstest.console.exe QuickFiler.Test.dll /InIsolation /TestCaseFilter:FullyQualifiedName~EfcFormControllerTests (assembly path excludes the .claude worktree tree); dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: +- Scoped run of EfcFormControllerTests: 44 passed, 0 failed, EXIT_CODE 0. +- New tests that pass: BindSourceFolderRows_WithViewerAndRouter_RetainsRowsAndBindsDocument, BindSourceFolderRows_NullRows_RetainsEmptyArray, BindSourceFolderRows_NullViewer_LeavesFolderRowsUnchanged, BindSourceFolderRows_NullRouter_LeavesFolderRowsUnchanged, BindFolderRows_NullRows_BindsEmptyDocumentWithoutRetaining, BindFolderRows_NullViewer_DoesNotBind, BindFolderRows_NullRouter_DoesNotBind, ApplyDeleteGestureAsync_NullViewer_RetainsTrashRowAndDoesNotBind (8 tests). +- Count discrepancy: the plan states 9 expected; the plan's own task list (P1-T1 to P1-T7 with P1-T6 as two methods) defines 8 tests. All 8 tasks' tests exist and pass. The "19 new tests" figure in P4-T5 is therefore 18 (8 + 7 + 3). +- All pre-existing EfcFormControllerTests tests pass. +- `dotnet tool run csharpier check .` after format: "Checked 1674 files in 4754ms.", exit 0. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-descendants-fail-before.2026-10-22T10-04.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-descendants-fail-before.2026-10-22T10-04.md new file mode 100644 index 000000000..3916b7e5d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-descendants-fail-before.2026-10-22T10-04.md @@ -0,0 +1,77 @@ +# Group A Fail-Before: EFC Descendant Tree (#1002) + +Timestamp: 2026-10-22T10-04 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; vstest.console.exe UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"FullyQualifiedName~BreadcrumbMessageCodecIssue1002Tests|FullyQualifiedName~BreadcrumbRowDescendantsIssue1002Tests|FullyQualifiedName~BreadcrumbHtmlRendererIssue1002Tests|FullyQualifiedName~BreadcrumbBridgeRouterIssue1002Tests|FullyQualifiedName~BreadcrumbBridgeRouterStaleCompletionIssue1002Tests" /ResultsDirectory:SCRATCH/results-issue-1002-p2t6 /Logger:"trx;LogFileName=issue-1002-p2t6.trx" +EXIT_CODE: 1 +ExpectedExitCode: 1 +Output Summary: +- Build: `0 Error(s)`, `0 Warning(s)` (first attempt failed with CS0104, an ambiguous `Match` between Moq and System.Text.RegularExpressions in the new router test file; fixed by full qualification and rebuilt). +- Test counts (derived from the trx): total 59, executed 59, passed 3, failed 56. Data rows count separately (the three malformed-nodeId codec tests carry two rows each). +- Every test in the Group A fail-before set is reported Failed (marked FB below). +- Deviation: the new QuickFiler.Test files carry no `#nullable enable`, because QuickFiler.Test compiles as C# 7.3 (no `LangVersion`), where the directive is a compile error; no QuickFiler.Test file carries it ("consistent with their neighbors", spec Constraints). The UtilitiesCS.Test files carry it. + +## Outcomes (one row per test result) + +| Outcome | Test | FB | Message (truncated) | +|---|---|---|---| +| Failed | DeserializeInbound_DescendantToggle_ReturnsNodeId | FB | threw BreadcrumbMessageException: Unknown inbound breadcrumb message type | +| Failed | DeserializeInbound_DescendantActivate_ReturnsNodeId | FB | threw BreadcrumbMessageException: Unknown inbound breadcrumb message type | +| Failed | DeserializeInbound_DescendantMessageWithoutNodeId_Throws (descendantToggle) | | message "Unknown inbound breadcrumb message type 'descendantToggle'." does not match *nodeId* | +| Failed | DeserializeInbound_DescendantMessageWithoutNodeId_Throws (descendantActivate) | | same, descendantActivate | +| Failed | DeserializeInbound_DescendantMessageWithEmptyNodeId_Throws (descendantToggle) | | same pattern | +| Failed | DeserializeInbound_DescendantMessageWithEmptyNodeId_Throws (descendantActivate) | | same pattern | +| Failed | DeserializeInbound_DescendantMessageWithNonStringNodeId_Throws (descendantToggle) | | same pattern | +| Failed | DeserializeInbound_DescendantMessageWithNonStringNodeId_Throws (descendantActivate) | | same pattern | +| Failed | SetLeafDescendants_KeyedChildren_BuildsOneNodePerChild | FB | Expected stored to be True because the active leaf has subfolders, but found False. | +| Failed | SetLeafDescendants_DuplicateDisplayNames_KeepsDistinctIdentities | | Expected nodes to contain 2 item(s), but found 0 | +| Passed | SetLeafDescendants_RowWithoutExpandableLeaf_ReturnsFalse | | | +| Failed | TryBeginDescendantExpand_KnownNodeWithChildren_ReturnsKeyAndGeneration | FB | fixture: SetLeafDescendants returned False | +| Failed | TryBeginDescendantExpand_UnknownNode_ReturnsFalse | | fixture: SetLeafDescendants returned False | +| Failed | TryBeginDescendantExpand_LeafNode_ReturnsFalse | | fixture: SetLeafDescendants returned False | +| Failed | TryCollapseDescendant_ExpandedNode_CollapsesIt | | fixture: SetLeafDescendants returned False | +| Failed | TryCollapseDescendant_CollapsedNode_ReturnsFalse | | fixture: SetLeafDescendants returned False | +| Failed | TryAttachDescendantChildren_MatchingGeneration_AttachesChildren | | fixture: SetLeafDescendants returned False | +| Failed | TryAttachDescendantChildren_SupersededGeneration_IsRejected | | fixture: SetLeafDescendants returned False | +| Failed | TryAttachDescendantChildren_CollapsedNode_IsRejected | | fixture: SetLeafDescendants returned False | +| Failed | TryAttachDescendantChildren_RemovedNode_IsRejected | | fixture: SetLeafDescendants returned False | +| Failed | TryAbandonDescendantExpand_CurrentGeneration_CollapsesNode | | fixture: SetLeafDescendants returned False | +| Failed | TryAbandonDescendantExpand_SupersededGeneration_LeavesNodeUnchanged | | fixture: SetLeafDescendants returned False | +| Failed | GetDescendantPath_KnownNode_ReturnsFolderPath | | fixture: SetLeafDescendants returned False | +| Failed | GetDescendantPath_UnknownNode_ReturnsNull | | fixture: SetLeafDescendants returned False | +| Failed | TrySelectDescendant_KnownNode_SetsSelectedDescendantId | | fixture: SetLeafDescendants returned False | +| Failed | TrySelectDescendant_UnknownNode_LeavesSelectionUnchanged | | fixture: SetLeafDescendants returned False | +| Failed | ActivateSegment_ClearsDescendantsAndSelection | | fixture: SetLeafDescendants returned False | +| Failed | CollapseAfter_ClearsDescendantsAndSelection | | fixture: SetLeafDescendants returned False | +| Failed | LeftArrow_ExpandedLeaf_ClearsDescendantsAndSelection | | fixture: SetLeafDescendants returned False | +| Failed | ToggleLeafExpanded_Collapse_ClearsDescendantsAndSelection | | fixture: SetLeafDescendants returned False | +| Failed | SetLeafChildren_KeylessChildren_HasNoDescendants | | fixture: SetLeafDescendants returned False | +| Failed | RenderRowFragment_KeyedChildWithChildren_RendersNodeAffordanceAfterName | FB | fixture: SetLeafDescendants returned False | +| Failed | RenderRowFragment_KeyedLeafChild_RendersNoAffordance | | fixture: SetLeafDescendants returned False | +| Failed | RenderRowFragment_ExpandedDescendant_NestsChildrenContainerAfterNode | | fixture: SetLeafDescendants returned False | +| Failed | RenderRowFragment_SelectedDescendant_MarksAriaSelected | | fixture: SetLeafDescendants returned False | +| Failed | RenderRowFragment_DescendantNames_AreHtmlEncoded | | SetLeafDescendants returned False | +| Passed | RenderRowFragment_KeylessChildren_RenderUnchanged | | | +| Failed | BridgeJs_NodeAffordanceBranch_PrecedesChildAndLeafBranches | FB | Expected nodeBranch to be greater than -1 because the node affordance branch exists, but found -1. | +| Failed | BridgeJs_DescendantActivateBranch_PrecedesKeylessChildBranch | | Expected keyedChild to be greater than -1, but found -1. | +| Failed | BridgeJs_NodeAffordanceEnterOrSpace_PrecedesRowConfirmation | | Expected space to be greater than -1, but found -1. | +| Failed | BaseCss_Child_IsFlexRow | FB | rule ".child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }" does not contain "display: flex" | +| Failed | DescendantToggle_SecondLevelNode_FetchesWithNodeKeyAndRendersChildren | FB | the latest render must carry a node for the Q3 path, but match.Success was False | +| Failed | DescendantToggle_ExpandedNode_CollapsesWithoutProviderCall | | latest render carries no node for Q3 | +| Failed | DescendantToggle_DoesNotChangeSelection | | latest render carries no node for Q3 | +| Failed | DescendantToggle_PostsNoSubfolderResult | | latest render carries no node for Q3 | +| Failed | DescendantActivate_Grandchild_CommitsArchiveRelativeStem | FB | latest render carries no node for Q3 | +| Failed | DescendantActivate_ThenRowConfirmed_RaisesConfirmationOnce | | latest render carries no node for Q3 | +| Failed | DescendantActivate_OutsideArchiveRoot_LeavesSelectionAndHighlightUnchanged | | latest render carries no node for Q3 | +| Failed | DescendantRequest_UnknownNode_IsRejectedWithoutStateChange | | latest render carries no node for Q3 | +| Failed | DescendantToggle_LeafNode_IsRejectedWithoutProviderCall | | latest render carries no node for Q4 | +| Failed | DescendantRequest_NodeFromAnotherRow_IsRejected | | latest render carries no node for Q3 | +| Failed | ExpandLeaf_KeyedChildren_RenderNodeAffordances | FB | Expected Regex.Matches(html, "data-role=\"node\"").Count to be 1, but found 0 | +| Failed | DescendantFetch_CompletesAfterCollapse_AttachesNothing | | latest render carries no node for Q3 | +| Failed | DescendantFetch_CompletesAfterRebind_PostsNothingForReplacedRow | | latest render carries no node for Q3 | +| Failed | DescendantFetch_ReverseCompletion_OnlyLatestGenerationApplies | | latest render carries no node for Q3 | +| Failed | DescendantFetch_ProviderFault_LeavesStateUnchangedAndAllowsRetry | | latest render carries no node for Q3 | +| Failed | DescendantFetch_Canceled_LeavesStateUnchanged | | latest render carries no node for Q3 | +| Failed | ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow | FB | Expected fixture.Posts.Count to be 0 because the replaced row receives no post, but found 2 | +| Passed | ExpandLeaf_CurrentRow_StillPostsSubfolderResultThenRender | | | + +Group A fail-before set (11): all Failed. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-descendants-pass-after.2026-10-22T10-08.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-descendants-pass-after.2026-10-22T10-08.md new file mode 100644 index 000000000..ca9d97693 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-descendants-pass-after.2026-10-22T10-08.md @@ -0,0 +1,38 @@ +# Group A Pass-After: EFC Descendant Tree (#1002) + +Timestamp: 2026-10-22T10-08 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; vstest.console.exe UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"FullyQualifiedName~Breadcrumb" /ResultsDirectory:SCRATCH/results-issue-1002-p3t10 /Logger:"trx;LogFileName=issue-1002-p3t10.trx" +EXIT_CODE: 0 +Output Summary: +- Build: `0 Error(s)`, `0 Warning(s)`. +- Test counts (derived from the trx): total 905, executed 905, passed 905, failed 0. The filter covers the five new Group A classes and every existing breadcrumb suite in both assemblies (including BreadcrumbHtmlRendererTests, BreadcrumbMessageCodecTests, BreadcrumbRowStateTests, BreadcrumbBridgeRouterTests and its partials, BreadcrumbBridgeRouterQueueTests, BreadcrumbBridgeRouterIssue439Tests, BreadcrumbBridgeRouterIssue614Tests, BreadcrumbBridgeRouterIssue995Tests). +- All 59 results of the five new Group A classes passed. +- Intermediate run: the first pass-after run reported 904 passed and 1 failed: `DescendantActivate_Grandchild_CommitsArchiveRelativeStem` asserted `data-depth="2"` for the grandchild, but the renderer numbers the first keyed level (Q3) as depth 0, so Invoices under Q3 is depth 1. The test expectation was corrected to depth 1 (a test-authoring error; the production depth numbering matches the plan, which places the first level at the row's children container). The rerun above is the recorded result. + +## Group A fail-before set: pass-after outcomes + +| Outcome | Test | +|---|---| +| Passed | DeserializeInbound_DescendantToggle_ReturnsNodeId | +| Passed | DeserializeInbound_DescendantActivate_ReturnsNodeId | +| Passed | SetLeafDescendants_KeyedChildren_BuildsOneNodePerChild | +| Passed | TryBeginDescendantExpand_KnownNodeWithChildren_ReturnsKeyAndGeneration | +| Passed | RenderRowFragment_KeyedChildWithChildren_RendersNodeAffordanceAfterName | +| Passed | BridgeJs_NodeAffordanceBranch_PrecedesChildAndLeafBranches | +| Passed | BaseCss_Child_IsFlexRow | +| Passed | DescendantToggle_SecondLevelNode_FetchesWithNodeKeyAndRendersChildren | +| Passed | DescendantActivate_Grandchild_CommitsArchiveRelativeStem | +| Passed | ExpandLeaf_KeyedChildren_RenderNodeAffordances | +| Passed | ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow | + +## Other task-named tests + +| Outcome | Test | +|---|---| +| Passed | Issue995BridgeJsPostsConfirmationForPlainEnterAndScrollsSelectedRowIntoView | +| Passed | DescendantActivate_OutsideArchiveRoot_LeavesSelectionAndHighlightUnchanged | +| Passed | ExpandLeaf_CurrentRow_StillPostsSubfolderResultThenRender | +| Passed | the five DescendantFetch_ tests (CompletesAfterCollapse, CompletesAfterRebind, ReverseCompletion, ProviderFault, Canceled) | +| Passed | the six RenderRowFragment_ tests, the three BridgeJs_ tests, all twenty-three BreadcrumbRowDescendantsIssue1002Tests, all eleven BreadcrumbBridgeRouterIssue1002Tests, all five codec test methods (eight data rows) | + +`QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` is 465 physical lines after the two case arms (at most 500). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-trash-fail-before.2026-10-22T10-10.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-trash-fail-before.2026-10-22T10-10.md new file mode 100644 index 000000000..4ef5f0d6b --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-trash-fail-before.2026-10-22T10-10.md @@ -0,0 +1,30 @@ +# Group B Fail-Before: EFC Trash Preselection (#1002) + +Timestamp: 2026-10-22T10-10 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"FullyQualifiedName~BreadcrumbBridgeRouterTrashIssue1002Tests|FullyQualifiedName~EfcFormControllerTests" /ResultsDirectory:SCRATCH/results-issue-1002-p4t4 /Logger:"trx;LogFileName=issue-1002-p4t4.trx" +EXIT_CODE: 1 +ExpectedExitCode: 1 +Output Summary: +- Build: `0 Error(s)`, `0 Warning(s)`. +- Test counts (derived from the trx): total 42, executed 42, passed 36, failed 6. The EfcFormControllerTests filter covers the class across its three partial files (data rows counted separately). +- Every test in the Group B fail-before set is reported Failed (FB); `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows` is reported Passed. +- `BreadcrumbBridgeRouterTrashIssue1002Tests.cs` was drafted on disk during Phase 2 but was not registered in `QuickFiler.Test/QuickFiler.Test.csproj` (so not compiled) until this phase; its registration and first compilation are part of P4-T2. + +## New Group B tests and the named existing test + +| Outcome | Test | FB | Message (truncated) | +|---|---|---|---| +| Failed | BindRows_PreferredTrashRow_SelectsTrashInDeliveredDocument | FB | Expected fixture.Router.SelectedFolderPath to be "Trash to Delete", but found . | +| Failed | BindRows_PreferredTrashRow_LastSelectionNotificationCarriesTrash | FB | Expected fixture.SelectionEvents.Count(path => path == Trash) to be 1, but found 0. | +| Passed | BindRows_PreferredTrashRow_PostsNoFollowUpRender | | | +| Passed | BindRows_PreferredRowAbsent_SelectsNothing | | | +| Passed | BindRows_NoPreferredRow_KeepsExistingClearBehavior | | | +| Failed | BindRows_PreferredTrashRowBeforeCoreInit_PendingDocumentCarriesSelection | FB | Expected the pending document to contain the selected trash rowwrap; it did not. | +| Failed | ActionDeleteAsync_WithRouter_SelectsTrashEntry | FB | Expected controller.SelectedFolder to be "Trash to Delete", but found . | +| Failed | ActionDeleteAsync_WithRouter_DeliveredDocumentMarksTrashSelected | FB | Expected documents.Last() to contain the selected trash rowwrap; it did not. | +| Failed | ActionDeleteAsync_AwaitedTwice_KeepsTrashSelectedAndSingleTrashRow | FB | Expected controller.SelectedFolder to be "Trash to Delete", but found . | +| Passed | ActionDeleteAsync_BindFault_IsReportedAtBindBoundary | | | +| Passed | ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows | existing | required Passed | +| Passed | AsyncVoidBoundary_WhenFaulted_LogsOnceAndDoesNotThrow (five data rows) | existing | | + +Group B fail-before set (6): all Failed. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-trash-pass-after.2026-10-22T10-12.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-trash-pass-after.2026-10-22T10-12.md new file mode 100644 index 000000000..f9a2a105a --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-trash-pass-after.2026-10-22T10-12.md @@ -0,0 +1,25 @@ +# Group B Pass-After: EFC Trash Preselection (#1002) + +Timestamp: 2026-10-22T10-12 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; vstest.console.exe UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"FullyQualifiedName~Breadcrumb|FullyQualifiedName~EfcFormController|FullyQualifiedName~EfcHomeController" /ResultsDirectory:SCRATCH/results-issue-1002-p5t3 /Logger:"trx;LogFileName=issue-1002-p5t3.trx" +EXIT_CODE: 0 +Output Summary: +- Build: `0 Error(s)`, `0 Warning(s)`. +- Test counts (derived from the trx): total 1009, executed 1009, passed 1009, failed 0. Covers every breadcrumb suite (including BreadcrumbBridgeRouterIssue614Tests and BreadcrumbBridgeRouterTests), EfcFormControllerTests (three partials), EfcFormControllerIssue995Tests, and EfcHomeControllerExecuteMovesTests. + +## Group B fail-before set and named tests: pass-after outcomes + +| Outcome | Test | +|---|---| +| Passed | BindRows_PreferredTrashRow_SelectsTrashInDeliveredDocument | +| Passed | BindRows_PreferredTrashRow_LastSelectionNotificationCarriesTrash | +| Passed | BindRows_PreferredTrashRowBeforeCoreInit_PendingDocumentCarriesSelection | +| Passed | ActionDeleteAsync_WithRouter_SelectsTrashEntry | +| Passed | ActionDeleteAsync_WithRouter_DeliveredDocumentMarksTrashSelected | +| Passed | ActionDeleteAsync_AwaitedTwice_KeepsTrashSelectedAndSingleTrashRow | +| Passed | BindRows_PreferredTrashRow_PostsNoFollowUpRender | +| Passed | BindRows_PreferredRowAbsent_SelectsNothing | +| Passed | BindRows_NoPreferredRow_KeepsExistingClearBehavior | +| Passed | ActionDeleteAsync_BindFault_IsReportedAtBindBoundary | +| Passed | ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows | +| Passed | AsyncVoidBoundary_WhenFaulted_LogsOnceAndDoesNotThrow (all five data rows) | diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/node-suites-run.2026-10-22T10-14.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/node-suites-run.2026-10-22T10-14.md new file mode 100644 index 000000000..1230fe237 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/node-suites-run.2026-10-22T10-14.md @@ -0,0 +1,16 @@ +# Node Suites: Order Isolation and FolderBreadcrumb.js Coverage (#1002) + +Timestamp: 2026-10-22T10-14 +Command: node --test --test-isolation=none --test-concurrency=1 --experimental-test-coverage --test-coverage-include=QuickFiler\Resources\FolderBreadcrumb.js --test-reporter=spec , run three times in one node process per order (launcher SCRATCH/issue-1002-node-orders.ps1) +EXIT_CODE: 0 +Output Summary: +- Every order: tests 80, pass 80, fail 0 (78 from the issue-997 suites plus the 2 new issue-1002 tests). +- `FolderBreadcrumb.js` coverage in every order: line 100.00 percent, branch 100.00 percent, funcs 100.00 percent; P0-T9 baseline was line 100.00 percent, so coverage is unchanged. + +| Run | File order | Seed | Exit | Tests / pass / fail | +|---|---|---|---|---| +| normal | issue-997-common, issue-997-recursive, issue-1002-nesting | n/a | 0 | 80 / 80 / 0 | +| reversed | issue-1002-nesting, issue-997-recursive, issue-997-common | n/a | 0 | 80 / 80 / 0 | +| shuffled | issue-1002-nesting, issue-997-recursive, issue-997-common | 1002 | 0 | 80 / 80 / 0 | + +The shuffle is a Fisher-Yates permutation driven by `System.Random(1002)` over the normal order. For seed 1002 the permutation coincides with the reversed order; it is recorded as produced, not re-drawn. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/qfc-nesting-fail-before.2026-10-22T10-13.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/qfc-nesting-fail-before.2026-10-22T10-13.md new file mode 100644 index 000000000..1f2bf95df --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/qfc-nesting-fail-before.2026-10-22T10-13.md @@ -0,0 +1,20 @@ +# Group C Fail-Before: QFC Nested Affordance and Indent (#1002) + +Timestamp: 2026-10-22T10-13 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"FullyQualifiedName~FolderBreadcrumbNestingIssue1002Tests" /ResultsDirectory:SCRATCH/results-issue-1002-p6t3 /Logger:"trx;LogFileName=issue-1002-p6t3.trx"; node --test --test-isolation=none --test-concurrency=1 --test-reporter=spec tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs +EXIT_CODE: 1 +ExpectedExitCode: 1 +Output Summary: +- Build: `0 Error(s)`, `0 Warning(s)` (the rebuild re-embeds the unchanged script resource). +- C# (derived from the trx): total 4, executed 4, passed 1, failed 3; vstest EXIT_CODE 1. +- Node: tests 2, pass 0, fail 2; node EXIT_CODE 1. +- Every Group C fail-before test is reported failing; `MakeSubfolders_PinnedRecursiveCallPrefixPreserved` is listed with its outcome (Passed). + +| Outcome | Test | FB | Message (truncated) | +|---|---|---|---| +| Failed | MakeSubfolders_AppendsNameBeforeToggle | FB | Expected name to be less than 1694 because the +/- follows the folder name, but found 1748. | +| Failed | MakeSubfolders_RecursiveCallPassesDepthPlusOne | FB | Expected passesDepth to be True because each nested level is one step deeper than its parent, but found False. | +| Failed | MakeSubfolders_SetsDataDepthAndPerDepthPadding | FB | Expected the makeSubfolders body to contain "data-depth"; it did not. | +| Passed | MakeSubfolders_PinnedRecursiveCallPrefixPreserved | | | +| Failed (node) | nested toggle follows the name at every depth | FB | AssertionError: the toggle of folder-option-0-node-Alpha follows its name (actual false, expected true) | +| Failed (node) | nested rows carry data-depth and per-depth padding | FB | AssertionError: Expected values to be strictly equal (actual null, expected '0') | diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/qfc-nesting-pass-after.2026-10-22T10-14.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/qfc-nesting-pass-after.2026-10-22T10-14.md new file mode 100644 index 000000000..366eaa903 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/qfc-nesting-pass-after.2026-10-22T10-14.md @@ -0,0 +1,19 @@ +# Group C Pass-After: QFC Nested Affordance and Indent (#1002) + +Timestamp: 2026-10-22T10-14 +Command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true; vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /InIsolation /TestCaseFilter:"FullyQualifiedName~FolderBreadcrumbNestingIssue1002Tests|FullyQualifiedName~FolderBreadcrumbAssetContractTests|FullyQualifiedName~FolderBreadcrumbRecursiveAssetIssue997Tests" /ResultsDirectory:SCRATCH/results-issue-1002-p7t2 /Logger:"trx;LogFileName=issue-1002-p7t2.trx"; node --test --test-isolation=none --test-concurrency=1 --test-reporter=spec tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs +EXIT_CODE: 0 +Output Summary: +- Build: `0 Error(s)`, `0 Warning(s)`. +- C# (derived from the trx): total 22, executed 22, passed 22, failed 0 across FolderBreadcrumbNestingIssue1002Tests, FolderBreadcrumbAssetContractTests, and FolderBreadcrumbRecursiveAssetIssue997Tests. +- Node: tests 2, pass 2, fail 0. +- Change confined to `makeSubfolders` in `QuickFiler/Resources/FolderBreadcrumb.js`: trailing `depth` parameter (zero when absent), `data-depth` and per-depth `padding-left` set on each `.sub`, name appended before the toggle block, recursive call `makeSubfolders(row, child.children, selectedChildOwnsActive, depth + 1)`. `render`, `makeRow`, the message listener, `FolderBreadcrumb.html`, and `Resources.resx` are unchanged; no function added. + +| Outcome | Test | FB | +|---|---|---| +| Passed | MakeSubfolders_AppendsNameBeforeToggle | FB | +| Passed | MakeSubfolders_RecursiveCallPassesDepthPlusOne | FB | +| Passed | MakeSubfolders_SetsDataDepthAndPerDepthPadding | FB | +| Passed | MakeSubfolders_PinnedRecursiveCallPrefixPreserved | | +| Passed (node) | nested toggle follows the name at every depth | FB | +| Passed (node) | nested rows carry data-depth and per-depth padding | FB | diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/baseline-per-file-coverage.2026-10-22T13-20.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/baseline-per-file-coverage.2026-10-22T13-20.md new file mode 100644 index 000000000..b75852256 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/baseline-per-file-coverage.2026-10-22T13-20.md @@ -0,0 +1,10 @@ +# Remediation baseline: per-file coverage (#1002, cycle 1) + +Timestamp: 2026-10-22T13-20 +Command: pwsh -NoProfile -File SCRATCH/issue-1002-r-perfile.ps1 -Files (union of class line nodes of the post-processed Cobertura; branch union takes the larger covered count per line) +EXIT_CODE: 0 +Output Summary: +- QuickFiler/Controllers/EfcFormController.Breadcrumb.cs: lines 78/121 (64.46%), branches 22/42 (52.38%). Matches expected 78/121 and 22/42. +- UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs: branches 7/14 (50.00%), lines 32/32. Matches expected 7/14. +- BreadcrumbMessages.cs condition-coverage of each `?? throw new ArgumentNullException` line: lines 84, 85, 124, 144, 173, 174 and 175 each 1/2. +- QuickFiler/Controllers/EfcFormController.Actions.cs (for the B-3 comparison): lines 26/111 (23.42%), branches 12/36 (33.33%). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/baseline-tests.2026-10-22T13-20.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/baseline-tests.2026-10-22T13-20.md new file mode 100644 index 000000000..b9084f0df --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/baseline-tests.2026-10-22T13-20.md @@ -0,0 +1,10 @@ +# Remediation baseline: coverage test route on unmodified branch head (#1002, cycle 1) + +Timestamp: 2026-10-22T13-20 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +EXIT_CODE: 0 +Output Summary: +- Test run outcome Completed: total 7579, executed 7579, passed 7579, failed 0 (skipped 0 derived as total minus executed; error, timeout, aborted, notExecuted 0). +- First-party coverage: lines 57527/66780 (86.14%), branches 14018/17439 (80.38%). +- Run against the already-built test assemblies of the branch head; no source change preceded it. +- Raw trx and Cobertura kept out of git (Cobertura and trx remain under the gitignored coverage output). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-baseline-per-file-coverage.2026-10-22T13-55.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-baseline-per-file-coverage.2026-10-22T13-55.md new file mode 100644 index 000000000..cd1f98907 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-baseline-per-file-coverage.2026-10-22T13-55.md @@ -0,0 +1,9 @@ +# Cycle 2 P0-T4: baseline per-file coverage + +Timestamp: 2026-10-22T13-55 +Command: SCRATCH/issue-1002-r2-perfile.ps1 -Files EfcFormController.Actions.cs,BreadcrumbDocumentAssets.cs on the post-processed Cobertura of P0-T3 +EXIT_CODE: 0 +Output Summary: +- EfcFormController.Actions.cs: lines 26/28 (92.86%), branches 13/16 (81.25%). +- BreadcrumbDocumentAssets.cs: lines 0/0, branches 0/0. The file holds string constants only (static class with const members), has no instrumented executable line, and has no class node in the Cobertura document; the figures are recorded as measured (no percentage exists). +- Repository first-party: lines 57542/66677 (86.30%), branches 14038/17419 (80.59%). diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-baseline-tests.2026-10-22T13-54.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-baseline-tests.2026-10-22T13-54.md new file mode 100644 index 000000000..8b0007efb --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-baseline-tests.2026-10-22T13-54.md @@ -0,0 +1,9 @@ +# Cycle 2 P0-T3: baseline tests with coverage at unmodified head + +Timestamp: 2026-10-22T13-54 +Command: pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . +EXIT_CODE: 0 +Output Summary: +- Test run outcome Completed: total 7597, executed 7597, passed 7597, failed 0 (error, timeout, aborted, notExecuted, inconclusive 0). +- First-party coverage: lines 57542/66677 (86.30%), branches 14038/17419 (80.59%). +- No raw trx or Cobertura committed. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-exclude-attribute-baseline.2026-10-22T13-52.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-exclude-attribute-baseline.2026-10-22T13-52.md new file mode 100644 index 000000000..82147c674 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-exclude-attribute-baseline.2026-10-22T13-52.md @@ -0,0 +1,8 @@ +# Cycle 2 P0-T5: ExcludeFromCodeCoverage reference counts at unmodified head + +Timestamp: 2026-10-22T13-52 +Command: git diff 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- '*.cs' | grep '^+' | grep -c ExcludeFromCodeCoverage ; git diff --stat 8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7 -- coverage.config (merge base of origin/main and HEAD) +EXIT_CODE: 0 +Output Summary: +- Added ExcludeFromCodeCoverage lines in *.cs: 5 +- git diff --stat of coverage.config against the merge base: empty diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-file-sizes.2026-10-22T13-51.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-file-sizes.2026-10-22T13-51.md new file mode 100644 index 000000000..e72d9f444 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/cycle2-file-sizes.2026-10-22T13-51.md @@ -0,0 +1,11 @@ +# Cycle 2 P0-T2: file sizes at unmodified head + +Timestamp: 2026-10-22T13-51 +Command: wc -l on each path (physical line count; each file ends with a newline, so equal to @(Get-Content).Count) +EXIT_CODE: 0 +Output Summary: +- QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs: 313 +- UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs: 343 +- UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs: 182 +- QuickFiler/Controllers/EfcFormController.Actions.cs: 206 +- All four at or under 500. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/file-sizes.2026-10-22T13-19.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/file-sizes.2026-10-22T13-19.md new file mode 100644 index 000000000..06ae90941 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/remediation-baseline/file-sizes.2026-10-22T13-19.md @@ -0,0 +1,10 @@ +# Remediation baseline: file sizes (#1002, cycle 1) + +Timestamp: 2026-10-22T13-19 +Command: @(Get-Content -LiteralPath ).Count for each file below +EXIT_CODE: 0 +Output Summary: +- QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs: 140 +- UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs: 297 +- UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs: 179 +- All three counts are at or under 500. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T10-39.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T10-39.md new file mode 100644 index 000000000..ff7eb5759 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T10-39.md @@ -0,0 +1,70 @@ +# Feature Audit: EFC and QFC folder tree selection and nesting (Issue #1002) + +- Review timestamp: 2026-10-22T10-39 +- Reviewer: feature-review agent + +## Scope and Baseline + +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002`, head `e96e65302de00d3b0f8174736c90a59f82525dc9`. +- Resolved base branch: `main` (`origin/main`), merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` (supplied by the caller and confirmed with `git merge-base main HEAD`). +- Diff: 75 files (`git diff --numstat 8e4ad1ecb...HEAD`): 12 production C# files (2 new), `QuickFiler/Resources/FolderBreadcrumb.js`, 8 C# test files, 1 node suite, 4 project files, feature documents and evidence, 2 task-researcher memory notes. +- Work mode: `full-bug` (`- Work Mode: full-bug` in `issue.md`); AC source: `spec.md` only. No `user-story.md` exists or is required. +- PR context: `artifacts/pr_context.summary.txt` and `.appendix.txt`, generated for head `e96e65302` (current). +- Evidence basis: executor projections under `evidence/`, the canonical coverage artifact `artifacts/csharp/coverage.xml` (byte-identical to the executor final Cobertura), and reviewer reads and scans of the diff. + +## Acceptance Criteria Inventory + +Source: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md`, `## Acceptance Criteria`. 26 checkbox items, AC1 to AC26, in four groups. All 26 were already checked off by the executor before this review. + +| Group | Criteria | +|---|---| +| A: EFC recursive subfolder expansion and selection | AC1 to AC10 | +| B: EFC trash button selects the trash entry | AC11 to AC13 | +| C: QFC nested row affordance and indentation | AC14 to AC16 | +| D: Cross-cutting scope, quality and evidence | AC17 to AC26 | + +## Acceptance Criteria Evaluation + +| AC | Verdict | Evidence | +|---|---|---| +| AC1 Codec contract | PASS | Codec diff registers both types and requires a non-empty string `nodeId`; `BreadcrumbMessageCodecIssue1002Tests` 8 of 8; `BreadcrumbMessageCodecTests` 26 of 26. | +| AC2 Row state | PASS | `BreadcrumbRow.Descendants.cs` read; `ClearDescendants` added at the four reset points in `BreadcrumbRow.cs`; `BreadcrumbDescendantNode.cs` absent from the diff; `BreadcrumbRowDescendantsIssue1002Tests` 24 of 24; `BreadcrumbRowStateTests` 22 of 22. | +| AC3 First-level expansion keeps keys | PASS | `.Arrows.cs` diff calls `SetLeafDescendants(children)` and still posts `subfolderResult` then the row render; `ExpandLeaf_KeyedChildren_RenderNodeAffordances` and `ExpandLeaf_CurrentRow_StillPostsSubfolderResultThenRender` pass. | +| AC4 Descendant toggle | PASS | `HandleDescendantToggleAsync` read: collapse without provider call, one fetch with the node key, one row render, no `subfolderResult`, no selection change; four named router tests pass. | +| AC5 Descendant activation is the filing destination | PASS | `ActivateDescendant` commits through `SelectHierarchyPath`; highlight applied only after root checks; `DescendantActivate_Grandchild_CommitsArchiveRelativeStem`, `..._ThenRowConfirmed_RaisesConfirmationOnce`, `..._OutsideArchiveRoot_LeavesSelectionAndHighlightUnchanged` pass. The descendant is marked in the posted render by attribute; its visual styling is code-review finding CR-B4, which the criterion text does not require. | +| AC6 Identity validation | PASS | Unknown row (existing `FindRow` path), unknown node, cross-row node and leaf toggle are logged and rejected; three named tests pass. | +| AC7 Descendant stale-completion guard | PASS | `ReferenceEquals(FindRow(row.RowId), row)` plus generation-checked `TryAttachDescendantChildren`; fault and cancellation call `TryAbandonDescendantExpand`; five barrier tests pass. | +| AC8 First-level stale-completion guard | PASS | Same-instance guard after the await in `ExpandLeafAsync`; `ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow` passes. | +| AC9 Recursive rendering | PASS | `AppendDescendants` read: identity, depth, name span, affordance after the name only with children, sibling nested container, HTML encoding, `aria-selected` on the selected row only, pinned prefix kept; 10 renderer tests and 15 existing renderer tests pass. | +| AC10 EFC bridge script contract | PASS | `BridgeJs` read: node branch precedes keyed, key-less and leaf branches; keyed branch precedes `renderedChildActivate`; Enter or Space on a node affordance returns before row confirmation; `.child` is a flex row. | +| AC11 Router preselection in the same bind | PASS | `SelectPreferredRow` runs before `DeliverDocument`, posts nothing, raises one change event; shares `TryGetRowSelection` with `SelectRow`; 7 trash router tests pass. | +| AC12 Router compatibility | PASS | Optional parameter; no-preference and absent-row tests pass; all existing router classes pass. | +| AC13 Controller end to end | PASS | `ApplyDeleteGestureAsync` awaits `BindBreadcrumbRowsAsync(_folderRows, TrashRowText)` with the null guard preserved; 4 controller tests and `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows` pass. | +| AC14 Affordance after the name | PASS | JS diff moves `appendChild(name)` before the toggle; `MakeSubfolders_AppendsNameBeforeToggle` passes. | +| AC15 One indent step per level | PASS | `depth = depth || 0`, `data-depth`, `padding-left: 24 + 16 * depth`, recursive `depth + 1`; `render`, `makeRow`, FolderBreadcrumb.html untouched; nesting tests and pinned-string suites pass. | +| AC16 Node behavioural suite | PASS | `node-suites-run.2026-10-22T10-14.md`: 80 of 80 in normal, reversed and shuffled order. | +| AC17 Fail-before and pass-after | PASS | Six regression-testing projections present with expected exit codes. | +| AC18 Scope fences | PASS | Reviewer filter of the diff for every excluded file: empty; no arrow-key additions in the EFC router or bridge script. | +| AC19 Thread affinity | PASS | Reviewer grep: 0 `ConfigureAwait(false)` and 0 `lock (` in `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs`; none added in `EfcFormController.Actions.cs`. | +| AC20 File size | PASS | Reviewer line counts: maximum 487 (test) and 471 (`BreadcrumbBridgeRouter.cs`); no near-limit test file in the diff. | +| AC21 Test policy and registration | PASS | Strict mocks 6 of 6, FluentAssertions only, no sleeps or temp files, Compile entries present; new names in the final executed list. | +| AC22 Existing tests | PASS | `final-toolchain-tests.2026-10-22T10-26.md` kept-green table: 0 failures; no existing test file in the diff. | +| AC23 Coverage | PASS | New files 100% line; every new method at least 90%; 292 of 292 changed lines covered; no exclusion added; repository 86.14% / 80.37% recorded against 80 / 75 and not lowered. (Whole-file sub-floor figures for three modified files are policy findings B-1 to B-3, outside this criterion's text.) | +| AC24 Toolchain | PASS | Loop pass 3 artifacts: format, analyzers, type check and tests, all exit 0, no file change. | +| AC25 Evidence form | PASS | No `.trx`, `.coverage`, `.xml` or Cobertura file in the diff; all evidence is Markdown under `evidence/`. | +| AC26 Manual Outlook check recorded | PASS | `manual-outlook-check.2026-10-22T10-17.md` states PENDING HUMAN INTERACTION and "Executed by an agent: no". | + +## Summary + +All 26 acceptance criteria PASS. The review verdict is nevertheless **REMEDIATION_REQUIRED** because of four blocking findings that sit outside the literal criterion texts: modified-file coverage below the CLAUDE.md floors (B-1 lowered by this change, B-2 and B-3 pre-existing) and the absent visual highlight for the selected EFC descendant (B-4), which would make the spec's manual validation step 3 fail. B-1, B-2 and B-4 are autonomous; B-3 requires a maintainer decision on the COM/VSTO/WinForms exemption or a seam follow-up. + +### Acceptance Criteria Status +- Source: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md` +- Total AC items: 26 +- Checked off (delivered): 26 +- Remaining (unchecked): 0 +- Items remaining: none + +## Acceptance Criteria Check-off + +All 26 items were already `- [x]` in `spec.md` when this review began, and each is evaluated PASS above, so no check-off change was made and none was reverted. No criterion text was modified. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T13-37.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T13-37.md new file mode 100644 index 000000000..4e2c5f93d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T13-37.md @@ -0,0 +1,72 @@ +# Feature Audit: EFC and QFC folder tree selection and nesting (Issue #1002) + +- Review timestamp: 2026-10-22T13-37 (re-audit after remediation cycle 1; prior audit `feature-audit.2026-10-22T10-39.md`) +- Reviewer: feature-review agent + +## Scope and Baseline + +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002`, head `b72ac1dbbfdb6b45d01a14dde9e91428a6bf41ac`. +- Resolved base branch: `main` (`origin/main`), merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` (supplied by the caller and confirmed with `git merge-base main HEAD` and `git merge-base origin/main HEAD`). +- Diff: 105 files (`git diff --numstat 8e4ad1ecb...HEAD`): 12 production C# files (2 new), `QuickFiler/Resources/FolderBreadcrumb.js`, 9 C# test files, 1 node suite, 4 project files, feature documents and evidence, 2 task-researcher and 2 feature-review memory notes. +- Work mode: `full-bug` (`- Work Mode: full-bug` in `issue.md`); AC source: `spec.md` only. No `user-story.md` is required. +- PR context: `artifacts/pr_context.summary.txt` and `.appendix.txt`, generated for head `b72ac1dbb` (current). +- Evidence basis: executor projections under `evidence/`, the canonical coverage artifact `artifacts/csharp/coverage.xml` (byte-identical to the cycle-1 run Cobertura), the cycle-1 trx counters and test outcomes, and reviewer reads and scans of the diff. + +## Acceptance Criteria Inventory + +Source: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md`, `## Acceptance Criteria`. 26 checkbox items, AC1 to AC26, in four groups. All 26 are checked `[x]` at head. + +| Group | Criteria | +|---|---| +| A: EFC recursive subfolder expansion and selection | AC1 to AC10 | +| B: EFC trash button selects the trash entry | AC11 to AC13 | +| C: QFC nested row affordance and indentation | AC14 to AC16 | +| D: Cross-cutting scope, quality and evidence | AC17 to AC26 | + +## Acceptance Criteria Evaluation + +Cycle 1 changed no behaviour covered by AC1 to AC19 other than adding styling and tests; those verdicts are re-confirmed against the head diff and the cycle-1 test run. + +| AC | Verdict | Evidence | +|---|---|---| +| AC1 Codec contract | PASS | Codec unchanged in cycle 1; `BreadcrumbMessageCodecIssue1002Tests` pass in the 7597-test run. | +| AC2 Row state | PASS | Unchanged in cycle 1; `BreadcrumbDescendantNode.cs` absent from the diff. | +| AC3 First-level expansion keeps keys | PASS | Unchanged; `BreadcrumbBridgeRouter.Arrows.cs:201` calls `SetLeafDescendants`. | +| AC4 Descendant toggle | PASS | Unchanged; router tests pass. | +| AC5 Descendant activation is the filing destination | PASS | Unchanged commit path; the selected descendant is now also visibly highlighted by the cycle-1 `.child[aria-selected="true"]` rules (cycle-0 CR-B4 resolved). | +| AC6 Identity validation | PASS | Unchanged; tests pass. | +| AC7 Descendant stale-completion guard | PASS | Unchanged; barrier tests pass. | +| AC8 First-level stale-completion guard | PASS | Unchanged. | +| AC9 Recursive rendering | PASS | Renderer unchanged; selection remains an attribute, now targeted by an attribute selector rather than a class. | +| AC10 EFC bridge script contract | PASS | `BridgeJs` unchanged; `.child` remains a flex row (`BaseCss_Child_IsFlexRow` and `BaseCss_ChildName_CarriesEllipsisRule` pass). | +| AC11 Router preselection in the same bind | PASS | Unchanged; trash router tests pass. | +| AC12 Router compatibility | PASS | Unchanged. | +| AC13 Controller end to end | PASS | `ApplyDeleteGestureAsync` unchanged; null-viewer arm now tested; controller tests pass. | +| AC14 Affordance after the name | PASS | `FolderBreadcrumb.js` unchanged in cycle 1. | +| AC15 One indent step per level | PASS | Unchanged. | +| AC16 Node behavioural suite | PASS | `node-suites-run.2026-10-22T10-14.md`; no JavaScript change since. | +| AC17 Fail-before and pass-after | PASS | Six cycle-0 projections present; cycle 1 adds `css-highlight-fail-before` and `css-highlight-pass-after`. | +| AC18 Scope fences | PASS | Reviewer filter of the diff for every excluded file: empty; `evidence/qa-gates/scope-fences-remediation.2026-10-22T13-33.md`. | +| AC19 Thread affinity | PASS | Reviewer grep: 0 `ConfigureAwait(false)` and 0 `lock (` in `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs`; cycle 1 adds none to `EfcFormController.Actions.cs`. | +| AC20 File size | PASS | Reviewer counts: maximum 487 (test) and 471 (`BreadcrumbBridgeRouter.cs`); none of the seven near-limit test files in Constraints is in the diff. | +| AC21 Test policy and registration | PASS | Cycle-1 tests use MSTest, FluentAssertions and the strict-mock fixture, no sleeps or files; `BreadcrumbMessagesIssue1002Tests.cs` registered in `UtilitiesCS.Test.csproj`; all 18 cycle-1 test names `outcome="Passed"` in the final trx. | +| AC22 Existing tests | PASS | 7597 of 7597 passed; no existing test file in the diff. | +| AC23 Coverage | FAIL | Coverage clauses are met (new files 100% line; every changed instrumented line covered; repository 86.30% / 80.58% recorded against 80 / 75 and not lowered in `coverage-comparison.2026-10-22T13-33.md`). The clause "no coverage exclusion is added" is not met as written: the diff adds five member-level `[ExcludeFromCodeCoverage]` attributes. The exemption is maintainer-ratified (B-3 decision, 2026-10-22T10:50Z), but AC23 was not amended to record it (finding R-1). | +| AC24 Toolchain | PASS | Cycle-1 complete pass in CLAUDE.md order with CLAUDE.md commands: `remediation-format`, `remediation-format-check`, `remediation-analyzers`, `remediation-typecheck`, `remediation-tests` (all 2026-10-22T13-32, exit 0, no file change on the last format run). The cycle-0 `final-toolchain` projections remain for the cycle-0 tree. | +| AC25 Evidence form | PASS | No `.trx`, `.coverage`, `.xml` or Cobertura file in the diff; all evidence is Markdown under `evidence/`. | +| AC26 Manual Outlook check recorded | PASS | `manual-outlook-check.2026-10-22T10-17.md` states PENDING HUMAN INTERACTION and "Executed by an agent: no". | + +## Summary + +25 of 26 acceptance criteria PASS; AC23 FAILS on its literal "no coverage exclusion is added" clause. All four cycle-0 blocking findings (B-1 to B-4) are resolved and verified against the canonical coverage artifact and the cycle-1 test run. The verdict is **REMEDIATION_REQUIRED** with one blocking finding, R-1 (autonomous): amend AC23 in `spec.md` to record the maintainer-ratified member-level exemption, after which AC23 can be re-evaluated PASS without any code change. + +### Acceptance Criteria Status +- Source: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md` +- Total AC items: 26 +- Checked off (delivered): 26 +- Remaining (unchecked): 0 +- Items remaining: none unchecked; AC23 is checked but evaluated FAIL in this review pending the R-1 amendment. + +## Acceptance Criteria Check-off + +All 26 items were already `- [x]` in `spec.md` when this review began. 25 are evaluated PASS, so no check-off change was needed for them. AC23 is evaluated FAIL; this review did not modify `spec.md` (no box changed, no criterion text changed). The remediation inputs direct the AC23 text amendment, after which the next review re-evaluates the criterion. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T14-10.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T14-10.md new file mode 100644 index 000000000..f1214c686 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T14-10.md @@ -0,0 +1,72 @@ +# Feature Audit: EFC and QFC folder tree selection and nesting (Issue #1002) + +- Review timestamp: 2026-10-22T14-10 (re-audit after remediation cycle 2; prior audits `feature-audit.2026-10-22T10-39.md` and `feature-audit.2026-10-22T13-37.md`) +- Reviewer: feature-review agent + +## Scope and Baseline + +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002`, head `54b36112abf0a4ac26384226694b02520842061b`. +- Resolved base branch: `main` (`origin/main`), merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` (supplied by the caller and confirmed with `git merge-base main HEAD` and `git merge-base origin/main HEAD`). +- Diff: 136 files (`git diff --stat 8e4ad1ecb...HEAD`): 12 production C# files (2 new), `QuickFiler/Resources/FolderBreadcrumb.js`, 9 C# test files, 1 node suite, 4 project files, feature documents and evidence, and agent memory notes. +- Work mode: `full-bug` (`- Work Mode: full-bug` in `issue.md`); AC source: `spec.md` only. No `user-story.md` is required. +- PR context: `artifacts/pr_context.summary.txt` and `.appendix.txt`, generated for head `54b36112a` (current). +- Evidence basis: executor projections under `evidence/`, the canonical coverage artifact `artifacts/csharp/coverage.xml` (cycle-2 run, written after the trx and after every cycle-2 source edit), the cycle-2 trx counters and test outcomes, and reviewer reads and scans of the diff. + +## Acceptance Criteria Inventory + +Source: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md`, `## Acceptance Criteria`. 26 checkbox items, AC1 to AC26, in four groups. All 26 are checked `[x]` at head. AC23 text was amended in cycle 2 (recorded under `## Planner Amendments`, 2026-10-22). + +| Group | Criteria | +|---|---| +| A: EFC recursive subfolder expansion and selection | AC1 to AC10 | +| B: EFC trash button selects the trash entry | AC11 to AC13 | +| C: QFC nested row affordance and indentation | AC14 to AC16 | +| D: Cross-cutting scope, quality and evidence | AC17 to AC26 | + +## Acceptance Criteria Evaluation + +Cycle 2 changed production behaviour only in the transient `subfolderResult` render of the EFC bridge script; all other verdicts are re-confirmed against the head diff and the cycle-2 test run (7599 of 7599 passed). + +| AC | Verdict | Evidence | +|---|---|---| +| AC1 Codec contract | PASS | Codec unchanged since cycle 0; `BreadcrumbMessageCodecIssue1002Tests` pass in the 7599-test run. | +| AC2 Row state | PASS | Unchanged; `BreadcrumbDescendantNode.cs` absent from the diff. | +| AC3 First-level expansion keeps keys | PASS | Router unchanged; the message sequence (`subfolderResult` then row render) is unchanged. Cycle 2 changes only how the client draws the transient `subfolderResult` children (name in a `span.name`), not the message sequence. | +| AC4 Descendant toggle | PASS | Unchanged; router tests pass. | +| AC5 Descendant activation is the filing destination | PASS | Unchanged; tests pass. | +| AC6 Identity validation | PASS | Unchanged; tests pass. | +| AC7 Descendant stale-completion guard | PASS | Unchanged; barrier tests pass. | +| AC8 First-level stale-completion guard | PASS | Unchanged. | +| AC9 Recursive rendering | PASS | `BreadcrumbHtmlRenderer.cs` has no cycle-2 change; `RenderRowFragment_KeylessChildren_RenderUnchanged` passes with its assertions unchanged, so key-less children still render exactly as before. The orchestrator AC9 ruling (remediation plan 2026-10-22T13-43) confines the N-5 wrap to the bridge script's client-side render, which AC9 does not govern. | +| AC10 EFC bridge script contract | PASS | The branch-order assertions of AC10 are untouched by the `subfolderResult` edit (it is in the message listener, not the click or keydown handlers); `BridgeJs_*` ordering tests and `BaseCss_Child_IsFlexRow` pass. | +| AC11 Router preselection in the same bind | PASS | Unchanged; trash router tests pass. | +| AC12 Router compatibility | PASS | Unchanged. | +| AC13 Controller end to end | PASS | `ApplyDeleteGestureAsync` unchanged; controller tests pass. | +| AC14 Affordance after the name | PASS | `FolderBreadcrumb.js` unchanged since cycle 0. | +| AC15 One indent step per level | PASS | Unchanged. | +| AC16 Node behavioural suite | PASS | `node-suites-run.2026-10-22T10-14.md`; no JavaScript asset change since. | +| AC17 Fail-before and pass-after | PASS | Six cycle-0 projections present; cycles 1 and 2 add their own fail-before and pass-after projections. | +| AC18 Scope fences | PASS | Reviewer filter of the diff for every excluded file: empty; `evidence/qa-gates/cycle2-scope-fences.2026-10-22T14-06.md`. No arrow-key handling added. | +| AC19 Thread affinity | PASS | Reviewer grep: 0 `ConfigureAwait(false)` and 0 `lock (` in `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs`; cycle 2 changed only a comment in `EfcFormController.Actions.cs`. | +| AC20 File size | PASS | Reviewer counts: maximum 487 (test) and 471 (`BreadcrumbBridgeRouter.cs`); cycle-2 files 339, 368, 185, 206; no near-limit test file from Constraints is in the diff. | +| AC21 Test policy and registration | PASS | Both cycle-2 tests use MSTest and FluentAssertions, the strict-mock fixture where mocks are needed, no sleeps, delays or files; both test names `outcome="Passed"` in the final trx. | +| AC22 Existing tests | PASS | 7599 of 7599 passed; no pre-existing test file is in the diff, and the cycle-2 helper change touches only the new `Issue1002` partial. | +| AC23 Coverage | PASS | New files 100% line (82/82, 63/63); every instrumented changed line covered; the only exclusions added are the five member-level attributes on the five members named in the amended text (reviewer count 5), with no `coverage.config` entry and no class-level exclusion; repository 86.31% / 80.59% recorded against 80 / 75 and not lowered in `cycle2-coverage-comparison.2026-10-22T14-06.md` (and against the branch baseline `baseline-coverage.2026-10-22T09-54.md`, 86.10% / 80.31%). Cycle-1 R-1 resolved. | +| AC24 Toolchain | PASS | Cycle-2 complete pass in CLAUDE.md order with CLAUDE.md commands: `cycle2-format`, `cycle2-format-check`, `cycle2-analyzers`, `cycle2-typecheck`, `cycle2-tests` (all 2026-10-22T14-06, exit 0, 0 warnings). | +| AC25 Evidence form | PASS | No `.trx`, `.coverage`, `.xml` or Cobertura file in the diff; all evidence is Markdown under `evidence/`. | +| AC26 Manual Outlook check recorded | PASS | `manual-outlook-check.2026-10-22T10-17.md` states PENDING HUMAN INTERACTION and "Executed by an agent: no". | + +## Summary + +26 of 26 acceptance criteria PASS. The cycle-1 blocking finding R-1 is resolved by the AC23 amendment and its dated planner-amendment record, and the optional items N-1, N-3 and N-5 were delivered with fail-before evidence for the two behavioural ones. No blocking finding remains; the verdict is **PASS**. The manual Outlook check (AC26) remains a non-blocking human step for the maintainer. + +### Acceptance Criteria Status +- Source: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md` +- Total AC items: 26 +- Checked off (delivered): 26 +- Remaining (unchecked): 0 +- Items remaining: none + +## Acceptance Criteria Check-off + +All 26 items were already `- [x]` in `spec.md` when this review began, and all 26 are evaluated PASS, so no check-off change was needed. This review did not modify `spec.md`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/issue.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/issue.md new file mode 100644 index 000000000..f6a750c72 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/issue.md @@ -0,0 +1,70 @@ +# efc-qfc-folder-tree-selection-and-nesting (Issue #1002) + +- Date captured: 2026-10-22 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/ (Issue #1002) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #1002 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/1002 +- Last Updated: 2026-10-22 +- Work Mode: full-bug + +## Summary + +Three defects in the QuickFiler folder-selection surfaces. (1) The EFC viewer does not support selecting a subfolder of a listed folder the way the QFC viewer now does after issue #997 (PR #998). (2) Pressing the EFC trash button creates a trash entry but does not select it. (3) In the QFC viewer, a subfolder that has its own subfolders shows its expand affordance (+/−) on the left instead of the right, and deeper levels are not indented beneath their visible parent. + +## Environment + +- OS/version: Windows 11, Outlook desktop (VSTO add-in), WebView2 breadcrumb surface +- Python version: not applicable (C# .NET Framework 4.8, JavaScript breadcrumb asset) +- Command/flags used: interactive use of the EFC and QFC viewers +- Data source or fixture: any mailbox whose folder tree has at least three nested levels + +## Steps to Reproduce + +1. Open the EFC viewer, open the folder suggestions, and try to drill into and select a subfolder of a suggested folder. +2. In the EFC viewer, press the trash button. +3. Open the QFC viewer, expand a suggested folder, then look at a child subfolder that itself has subfolders and expand it. + +## Expected Behavior + +1. EFC offers the same recursive subfolder expansion and selection that QFC gained in #997, and the selected subfolder becomes the filing destination. +2. After the trash button adds the trash entry, that entry is the selected entry. +3. In QFC, the expand affordance of every expandable subfolder row sits on the right of the row, matching the top-level rows, and each nested level is indented one step further than its visible parent. + +## Actual Behavior + +1. EFC does not offer recursive subfolder selection. +2. The trash entry is created but the selection stays where it was. +3. The expand affordance of a nested subfolder renders on the left, and nested levels share one indent. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: reported by the maintainer on 2026-10-22; no log excerpt. + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +## Suspected Cause / Notes + +- The #997 fix (merge 59b888d38) added descendant support in `QuickFiler/Resources/FolderBreadcrumb.js`, `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.Descendants.cs`, `BreadcrumbSelectionSession.Descendants.cs`, `QuickFiler/Viewers/BreadcrumbBridgeCoordinator.Descendants.cs`, and wiring in `QfcItemController.BreadcrumbWiring.cs` and `ItemViewer.Breadcrumb.cs`. The EFC path (`EfcViewer`, `EfcFormController`) is likely not wired to the descendant messages. +- Trash handling lives in `EfcFormController.Actions.cs` and `EfcDataModel.cs`. +- The subfolder toggle is appended after the label in `FolderBreadcrumb.js` (around the `subfolderElement.appendChild(toggle)` call); the left placement and flat indent are likely CSS or DOM-order issues in the descendant row rendering. + +## Proposed Fix / Validation Ideas + +- [ ] Unit coverage areas: EFC descendant wiring, trash selection, descendant row rendering order and depth indentation +- [ ] Integration scenario to retest: three-level folder tree in EFC and QFC +- [ ] Manual verification notes: affordance on the right at every level; indent increases per level + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/plan.2026-10-22T05-08.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/plan.2026-10-22T05-08.md new file mode 100644 index 000000000..0bea33d16 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/plan.2026-10-22T05-08.md @@ -0,0 +1,235 @@ +# 2026-10-22-efc-qfc-folder-tree-selection-and-nesting (Plan) + +- **Issue:** #1002 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-10-22T05-08 +- **Status:** Ready for preflight +- **Version:** 0.2 +- **Work Mode:** full-bug (acceptance-criteria source: `spec.md` only, AC1 to AC26) +- **Branch:** bug/efc-qfc-folder-tree-selection-and-nesting-1002 + +**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. + +## Terms, Routes, and Conventions + +- `FEATURE` names the folder `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002`; every evidence path below is `/evidence//..md`, stamped with the UTC write time per `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`. A consumer selects the lexicographically greatest `.*.md`. +- `SCRATCH` means `/issue-1002/`. Every scratch file, staged copy, trx, raw coverage document, and helper script lives under it with a name starting `issue-1002-`. Artifacts record the literal token `SCRATCH`, never a host path, host name, or account name. +- Tool routes, launchers, command-text restrictions, and hook context are owned by `.claude/skills/execution-environment/SKILL.md` and are not restated here. Its CI section describes the upstream repository; the TaskMaster CI checks are actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, and pester, and none of them runs the node `.mjs` suites. +- Toolchain steps are cited by name and run with the exact `CLAUDE.md` commands: FORMAT (`dotnet tool run csharpier format .`, verified by `dotnet tool run csharpier check .`), ANALYZE (the `/t:Rebuild` analyzer command), TYPECHECK (the `/t:Rebuild` warnings-as-errors command, without `/p:Nullable=enable`), and TEST-COVERAGE (`scripts/vscode/Invoke-MSTestWithCoverage.ps1` with `-SearchRoot .`). MSBuild is resolved through vswhere and given the `CLAUDE.md` argument list directly; `scripts/vscode/Invoke-VSBuild.ps1` is not used, because its package-reference sync rewrites HintPath elements in every project file and would put project files outside the Write Set into the diff. +- SCOPED-TEST is a `vstest.console.exe` run over the built `UtilitiesCS.Test` and `QuickFiler.Test` assemblies with `/InIsolation`, a `/TestCaseFilter` naming the classes the task lists, and a results directory and trx file under `SCRATCH`; assembly discovery excludes any path containing the `.claude` worktree segment. Evidence records a summary derived from the trx (counts plus one outcome row per named test), never the trx itself. +- Local shell-icon carve-out: four UtilitiesCS.Test classes (`ShellUtilities_Tests`, `ShellUtilitiesStatic_Tests`, `SysImageListHelperTests`, `OSBrowser_Tests`) stall locally on SHGetFileInfo. If a TEST-COVERAGE run stalls on them, the executor records the stall, re-runs the same `dotnet-coverage` collection over a direct vstest invocation whose filter adds exclusions for exactly those four classes, and records the filter in the artifact. CI covers those classes. No other exclusion is permitted. +- Every command-step artifact carries `Timestamp:`, `Command:`, `EXIT_CODE:`, and `Output Summary:`; an artifact that records an expected non-zero gate carries `ExpectedExitCode:`. Committed evidence is Markdown only: trx, Cobertura, JaCoCo, and binary coverage outputs stay under the ignored `coverage/` folder or `SCRATCH`. +- Evidence naming: the spec's Test Strategy lists unstamped paths; this plan stamps every artifact. The spec stems `baseline-toolchain` and `final-toolchain` are realized as one artifact per command step (`baseline-toolchain-`, `final-toolchain-`), because the atomic-plan contract forbids aggregate-only command artifacts. +- Commits are owned by the orchestrator; this plan carries no commit task. Scope gates therefore pair a merge-base diff with `git status --porcelain` so uncommitted and untracked paths are both observed. +- Every new C# file carries `#nullable enable` and an explicit `` in its legacy project file. No new C# test creates a file, sleeps, delays, or constructs a real `WebView2BreadcrumbHost`; mocks of `IFolderHierarchyProvider` and `IBreadcrumbWebHost` use `MockBehavior.Strict`; ordering uses `TaskCompletionSource` barriers. +- Write-set fence: only the files in the spec `## Write Set` change. A task that finds it needs any other file halts and reports to the orchestrator. + +## Test Name Register + +The spec names every test except those in the row-descendants file; the plan names them here. Names are digit-free and binding. + +- `BreadcrumbRowDescendantsIssue1002Tests`: "SetLeafDescendants_KeyedChildren_BuildsOneNodePerChild", "SetLeafDescendants_DuplicateDisplayNames_KeepsDistinctIdentities", "SetLeafDescendants_RowWithoutExpandableLeaf_ReturnsFalse", "TryBeginDescendantExpand_KnownNodeWithChildren_ReturnsKeyAndGeneration", "TryBeginDescendantExpand_UnknownNode_ReturnsFalse", "TryBeginDescendantExpand_LeafNode_ReturnsFalse", "TryCollapseDescendant_ExpandedNode_CollapsesIt", "TryCollapseDescendant_CollapsedNode_ReturnsFalse", "TryAttachDescendantChildren_MatchingGeneration_AttachesChildren", "TryAttachDescendantChildren_SupersededGeneration_IsRejected", "TryAttachDescendantChildren_CollapsedNode_IsRejected", "TryAttachDescendantChildren_RemovedNode_IsRejected", "TryAbandonDescendantExpand_CurrentGeneration_CollapsesNode", "TryAbandonDescendantExpand_SupersededGeneration_LeavesNodeUnchanged", "GetDescendantPath_KnownNode_ReturnsFolderPath", "GetDescendantPath_UnknownNode_ReturnsNull", "TrySelectDescendant_KnownNode_SetsSelectedDescendantId", "TrySelectDescendant_UnknownNode_LeavesSelectionUnchanged", "ActivateSegment_ClearsDescendantsAndSelection", "CollapseAfter_ClearsDescendantsAndSelection", "LeftArrow_ExpandedLeaf_ClearsDescendantsAndSelection", "ToggleLeafExpanded_Collapse_ClearsDescendantsAndSelection", "SetLeafChildren_KeylessChildren_HasNoDescendants". +- Node suite `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs`: "nested toggle follows the name at every depth" and "nested rows carry data-depth and per-depth padding". +- Group A fail-before set (must fail before P3 and pass after): `DeserializeInbound_DescendantToggle_ReturnsNodeId`, `DeserializeInbound_DescendantActivate_ReturnsNodeId`, `SetLeafDescendants_KeyedChildren_BuildsOneNodePerChild`, `TryBeginDescendantExpand_KnownNodeWithChildren_ReturnsKeyAndGeneration`, `RenderRowFragment_KeyedChildWithChildren_RendersNodeAffordanceAfterName`, `BridgeJs_NodeAffordanceBranch_PrecedesChildAndLeafBranches`, `BaseCss_Child_IsFlexRow`, `DescendantToggle_SecondLevelNode_FetchesWithNodeKeyAndRendersChildren`, `DescendantActivate_Grandchild_CommitsArchiveRelativeStem`, `ExpandLeaf_KeyedChildren_RenderNodeAffordances`, `ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow`. +- Group B fail-before set: `BindRows_PreferredTrashRow_SelectsTrashInDeliveredDocument`, `BindRows_PreferredTrashRow_LastSelectionNotificationCarriesTrash`, `BindRows_PreferredTrashRowBeforeCoreInit_PendingDocumentCarriesSelection`, `ActionDeleteAsync_WithRouter_SelectsTrashEntry`, `ActionDeleteAsync_WithRouter_DeliveredDocumentMarksTrashSelected`, `ActionDeleteAsync_AwaitedTwice_KeepsTrashSelectedAndSingleTrashRow`. +- Group C fail-before set: `MakeSubfolders_AppendsNameBeforeToggle`, `MakeSubfolders_RecursiveCallPassesDepthPlusOne`, `MakeSubfolders_SetsDataDepthAndPerDepthPadding`, and both node-suite tests above. +- New tests outside a fail-before set are characterization or guard tests; each fail-before artifact lists their pre-fix outcome without requiring failure. + +## Pre-Mortem + +- Review checklists: the final phase walks the policy-audit, code-review, and feature-audit checklists into `/evidence/qa-gates/self-review..md` (P9-T34); fail-before and pass-after artifacts exist per defect group (P2-T6, P3-T10, P4-T4, P5-T3, P6-T3, P7-T2); the near-limit files are guarded by P8-T3. +- Coverage: baseline (P0-T6) and final (P9-T4) TEST-COVERAGE runs record repository line and branch figures against the 80 and 75 percent floors, and P9-T5 records per-file and per-method line coverage for every new partial and new method (at least 90 percent) and changed-line coverage derived mechanically from the merge-base diff; no coverage exclusion is added. `FolderBreadcrumb.js` is outside the C# tool, so its node coverage figure is recorded at P0-T9 and P7-T3. +- Mirror parity: not applicable. No `.claude`, `.agents`, `.codex`, `.github`, or bundled resource file changes, and no fact stated by the execution-environment brief (CI jobs, required contexts, tool routes, batch budget) is altered, so no brief-update task exists. +- File size: `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` receives only two delegating case arms and the preferred-row parameter threading; all new logic goes into new partials; new tests go into new files; P0-T7 and P8-T3 record physical line counts and confirm the seven near-limit test files are absent from the diff. +- Test isolation: Jest, Python, and Pester isolation runs are not applicable because no TypeScript, Python, or PowerShell file changes. The plan instead runs each new C# test class in its own fresh process in reverse register order plus all new classes in one process (P8-T5), and runs the node suites in normal, reversed, and seeded shuffled file order with the seed recorded (P7-T3). Tests share no static state; no real WebView2 host is constructed, because its static owner table is shared under class-level parallel execution. +- CI-only behavior: CI runs no node suite and no browser engine, so every JavaScript behavior has a C# text-contract test that runs under mstest-coverage (P2-T3 for `BridgeJs` and `BaseCss`, P6-T1 for `makeSubfolders`). New files carry `#nullable enable` so build-nullable sees them, and FORMAT runs before every gate so format-check passes. +- Evidence: every artifact path is stated per task under `/evidence//`, carries the four required fields, records `SCRATCH` instead of host paths, and contains no raw trx or coverage document (P9-T6 verifies). The manual Outlook check is recorded as pending human interaction and never claimed as executed (P8-T6). + +## Size Justification + +The plan holds 81 tasks and exceeds the 400-line guideline; planning documents are exempt from the 500-line source-file limit, and the plan is not compressed. It covers three defect groups, each with fail-before tests and a fix, across UtilitiesCS, QuickFiler, and the QFC JavaScript asset. Three independent defects each need a failing-test phase and a fix phase under the bugfix workflow, Group A spans nine production files across two assemblies, the final phase carries one check-off task per acceptance criterion as the acceptance-criteria-tracking protocol requires (26 tasks), and every evidence artifact the spec names has its own producing task. Splitting the item would separate the shared router file edits across items and break the single write-set fence. + +### Phase 0 — Baseline Capture and Policy Reads + +- [x] [P0-T1] Read, in `policy-compliance-order` order, `CLAUDE.md`, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/csharp.md`, then `.claude/rules/plan-acceptance-gates.md`, `.claude/skills/execution-environment/SKILL.md`, `.claude/skills/evidence-and-timestamp-conventions/SKILL.md`, `.claude/skills/acceptance-criteria-tracking/SKILL.md`, `/spec.md`, and the research record under `/research/`. Write `/evidence/baseline/phase0-instructions-read..md` with `Timestamp:`, `Policy Order:`, the branch name, and the list of files read. Acceptance: the artifact exists and names every file in this task. +- [x] [P0-T2] Bootstrap the worktree toolchain with `scripts/vscode/Install-RepoDotNetSdk.ps1`, `dotnet tool restore`, and `scripts/vscode/Invoke-Restore.ps1`; write `/evidence/baseline/baseline-toolchain-bootstrap..md`. Acceptance: each command records `EXIT_CODE: 0`, and `git status --porcelain` captured after the restore lists no tracked-file change. +- [x] [P0-T3] Record the read-only FORMAT baseline by running only `dotnet tool run csharpier check .` (no `format`, so pre-existing drift is recorded rather than repaired); write `/evidence/baseline/baseline-toolchain-format..md`. Acceptance: the artifact records the exit code and either the success line beginning `Checked ` or the list of drifting files. +- [x] [P0-T4] Run ANALYZE on `TaskMaster.sln`; write `/evidence/baseline/baseline-toolchain-analyzers..md`. Acceptance: the artifact records the exit code, the `Error(s)` and `Warning(s)` counts, and every warning whose path is a Write Set file (expected none). +- [x] [P0-T5] Run TYPECHECK on `TaskMaster.sln`; write `/evidence/baseline/baseline-toolchain-typecheck..md`. Acceptance: the artifact records the exit code and the `Error(s)` count. +- [x] [P0-T6] Run TEST-COVERAGE through `scripts/vscode/Invoke-MSTestWithCoverage.ps1` (shell-icon carve-out applies); write `/evidence/baseline/baseline-coverage..md`. Acceptance: the artifact records passed, failed, and total counts, the one-line first-party coverage summary, numeric repository line and branch percentages, and numeric per-file line coverage for each existing Write Set production `.cs` file (`BreadcrumbRow.cs`, `BreadcrumbMessages.cs`, `BreadcrumbMessageCodec.cs`, `BreadcrumbHtmlRenderer.cs`, `BreadcrumbDocumentAssets.cs`, `BreadcrumbBridgeRouter.cs`, `BreadcrumbBridgeRouter.Arrows.cs`, `BreadcrumbBridgeRouter.Selection.cs`, `EfcFormController.Actions.cs`, `EfcFormController.Breadcrumb.cs`); no value is a placeholder. Because the coverage script writes its post-processed Cobertura document to a fixed path that P9-T4 overwrites, copy that document to `SCRATCH` as `issue-1002-baseline-cobertura.xml` immediately after this run, so P9-T5 can read the baseline per-line data; the copy is never committed. +- [x] [P0-T7] Record physical line counts (a physical count, not `Measure-Object -Line`) for every Write Set code file, including `QuickFiler/Resources/FolderBreadcrumb.js`, and for the seven near-limit test files the spec lists under Constraints; write `/evidence/baseline/baseline-file-sizes..md`. Acceptance: one numeric count per file. +- [x] [P0-T8] Search `UtilitiesCS.Test/`, `QuickFiler.Test/`, and `tests/quickfiler-breadcrumb/` for `GetField(`, `GetMethod(`, and `GetProperty(` combined with each member this plan changes (`ApplyDeleteGesture`, `ActionDeleteAsync`, `SelectHierarchyPath`, `SelectRow`, `ExpandLeafAsync`, `BindRowsAsync`, `BindBreadcrumbRowsAsync`, `SetLeafChildren`, `AppendChildren`, `IsKnownInboundType`, `makeSubfolders`); write `/evidence/baseline/baseline-test-disposition..md` with one row per matching test and its disposition. Acceptance: the artifact lists `AsyncVoidBoundary_WhenFaulted_LogsOnceAndDoesNotThrow` (reaches `ButtonDeleteClickAsync` by name; unchanged, because `ActionDeleteAsync` keeps awaiting the viewer UI context as its first statement) and the comment-only `ApplyDeleteGesture` reference in `QuickFiler.Test/Controllers/EfcFormControllerTests.cs` (unchanged); any further match carries a disposition, and a match that must change halts the plan for a planner revision. +- [x] [P0-T9] Run `tests/quickfiler-breadcrumb/issue-997-common.test.mjs` and `tests/quickfiler-breadcrumb/issue-997-recursive.test.mjs` with the node test runner and experimental coverage limited to `QuickFiler/Resources/FolderBreadcrumb.js`; write `/evidence/baseline/baseline-node-suites..md`. Acceptance: the artifact records pass and fail counts and the numeric line coverage of `FolderBreadcrumb.js`. + +### Phase 1 — Group A Declaration-Only Seams + +The Group A regression tests reference members that do not exist yet. These tasks declare them with no behavior, so the regression tests compile and fail at assertion time rather than at build time. + +- [x] [P1-T1] In `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs`, add `BreadcrumbMessageTypes.DescendantToggle` with value "descendantToggle" and `BreadcrumbMessageTypes.DescendantActivate` with value "descendantActivate", and an optional trailing constructor parameter `string? nodeId = null` exposed as `BreadcrumbInboundMessage.NodeId` with XML documentation. `BreadcrumbMessageCodec.cs` is not edited here, so both types remain unknown to the codec. Acceptance: P1-T3 builds with every existing constructor call unchanged. +- [x] [P1-T2] Add `partial` to `BreadcrumbRow` in `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` and create `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs`, registered in `UtilitiesCS/UtilitiesCS.csproj`, declaring with no behavior: public `SetLeafDescendants(IReadOnlyList)`, `TryBeginDescendantExpand(string nodeId, out FolderTreeNodeKey? key, out int generation)`, `TryCollapseDescendant(string)`, `TryAttachDescendantChildren(string, int, IReadOnlyList)`, and `TryAbandonDescendantExpand(string, int)`, each returning false; `GetDescendantPath(string)` returning null; a get-only `SelectedDescendantId` that stays null with `TrySelectDescendant(string?)` returning false; and an internal `DescendantNodes` returning an empty `IReadOnlyList`. `BreadcrumbDescendantNode.cs` is not edited. Acceptance: P1-T3 builds. +- [x] [P1-T3] Run ANALYZE on `TaskMaster.sln` after the seams; write `/evidence/other/seam-build-efc-descendants..md`. Acceptance: `EXIT_CODE: 0` and a `0 Error(s)` line. + +### Phase 2 — Group A Regression Tests (fail first) + +Each new test file is registered in its test project in the same task. Router tests drive the real `BreadcrumbBridgeRouter` through `ProcessInboundAsync` with inbound JSON and read node identities from the `data-node-id` attributes of captured render posts. + +- [x] [P2-T1] [expect-fail] Create `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` (registered in `UtilitiesCS.Test/UtilitiesCS.Test.csproj`) with the five codec tests the spec names. Acceptance: the class compiles in P2-T6 and holds exactly those five test names. +- [x] [P2-T2] [expect-fail] Create `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` (registered) with the twenty-three row tests in the Test Name Register, using `FolderBreadcrumbSegment` fixtures with distinct `FolderTreeNodeKey` values and reading node identities through the internal `DescendantNodes`. Acceptance: the class compiles in P2-T6 and holds every register name. +- [x] [P2-T3] [expect-fail] Create `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` (registered) with the ten renderer and asset tests the spec names. The `BridgeJs` tests assert presence and index order over `BreadcrumbDocumentAssets.BridgeJs`: the click-listener tokens `.affordance[data-role="node"]` before `.child[data-node-id]` before `.child[data-child-index]` before `type: 'leafExpandToggle'`, and `type: 'descendantToggle'` and `type: 'descendantActivate'` present; in the keydown listener the token `e.key === ' '` before `type: 'rowConfirmed'`. `BaseCss_Child_IsFlexRow` asserts the `.child {` rule carries `display: flex` and `align-items: center`. Acceptance: the class compiles in P2-T6 and holds all ten names. +- [x] [P2-T4] [expect-fail] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` (registered in `QuickFiler.Test/QuickFiler.Test.csproj`) with the eleven router tests the spec names, over strict mocks of `IFolderHierarchyProvider` and `IBreadcrumbWebHost`; the archive-root fixture binds root `\\mailbox\Archive` and presented row `Clients\North`, and activation of the grandchild `Invoices` under `Q3` expects `SelectedFolderPath` to equal `Clients\North\Q3\Invoices`. Acceptance: the class compiles in P2-T6 and holds all eleven names. +- [x] [P2-T5] [expect-fail] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs` (registered) with the seven stale-completion tests the spec names, each holding the provider task open on a `TaskCompletionSource` and completing it after the collapse, rebind, or later toggle. Acceptance: the class compiles in P2-T6 and holds all seven names. +- [x] [P2-T6] [expect-fail] Rebuild `TaskMaster.sln` with ANALYZE, then run SCOPED-TEST over the five Group A classes; write `/evidence/regression-testing/efc-descendants-fail-before..md` with `ExpectedExitCode: 1`. Acceptance: the build reports `0 Error(s)`; every test in the Group A fail-before set is reported Failed with its assertion or exception message recorded; every other new Group A test is listed with its outcome. + +### Phase 3 — Group A Fix (EFC descendant tree, messages, rendering, router, stale guards) + +- [x] [P3-T1] In `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs`, register both new types in `IsKnownInboundType` and, for both, read `nodeId` through `OptionalString` (a non-string fails) and fail fast through `Fail` when it is null or empty, in the pattern of the `childIndex` check; pass it as `NodeId` and update the `DeserializeInbound` XML documentation. Acceptance: the five codec tests and the unchanged `BreadcrumbMessageCodecTests` class pass in P3-T10. +- [x] [P3-T2] Implement `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs`: `SetLeafDescendants` returns false unless the active segment can expand, otherwise stores the `BreadcrumbRowBuilder.MapSegments` projection as the first-level children and builds one `BreadcrumbDescendantNode` per keyed child; `TryBeginDescendantExpand` succeeds only for a node in this row's tree that has children and is not expanded, marking it expanded, advancing `RequestGeneration`, and returning its key and generation; `TryCollapseDescendant` collapses an expanded node; `TryAttachDescendantChildren` attaches only when the node is still in the tree, still expanded, and its generation matches; `TryAbandonDescendantExpand` collapses only on a matching generation; `GetDescendantPath` returns the node's `FolderPath` or null; `TrySelectDescendant` accepts null (clear) or a known identity and leaves `SelectedDescendantId` unchanged otherwise. Acceptance: the twenty-three row tests pass in P3-T10. +- [x] [P3-T3] In `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs`, clear the descendant nodes and `SelectedDescendantId` in `ActivateSegment` where `_leafChildren` is replaced, in `CollapseAfter` and `LeftArrow` where `IsLeafExpanded` is set false, in `ToggleLeafExpanded` when it collapses, and in `SetLeafChildren` so key-less rows carry no descendants. Acceptance: the four reset tests, `SetLeafChildren_KeylessChildren_HasNoDescendants`, and the unchanged `BreadcrumbRowStateTests` class pass in P3-T10. +- [x] [P3-T4] In `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs`, make `AppendChildren` render recursively from `DescendantNodes` when the expanded row has any, and exactly as today otherwise: each node is a `div` with the preserved prefix `class="child" data-child-index=` followed by `data-node-id`, `data-depth`, an encoded `title`, and `aria-selected="true"` only when the row is the selected row and the node is `SelectedDescendantId`; inside it an encoded name `span` and then, only for a node with children, `` holding its children at the next depth. Acceptance: the six `RenderRowFragment_` tests and the unchanged `BreadcrumbHtmlRendererTests` class pass in P3-T10. +- [x] [P3-T5] In `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs`, merge `display: flex; align-items: center;` into the existing `.child` rule of `BaseCss`; in the `BridgeJs` click listener add, before the `.child[data-child-index]` branch, a `.affordance[data-role="node"]` branch that posts `type: 'descendantToggle'` with the row id and the enclosing `data-node-id`, stops propagation, and returns, followed by a `.child[data-node-id]` branch that posts `type: 'descendantActivate'` and stops propagation; keep the key-less branch and the leaf branch; in the keydown listener add, before the Enter-confirms-row branch, an Enter or Space (`e.key === ' '`) branch on a focused node affordance that posts `descendantToggle`, prevents default, stops propagation, and returns; add no arrow-key handling. Acceptance: the two `BridgeJs_` ordering tests, the keydown ordering test, `BaseCss_Child_IsFlexRow`, and `Issue995BridgeJsPostsConfirmationForPlainEnterAndScrollsSelectedRowIntoView` pass in P3-T10. +- [x] [P3-T6] In `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`, change `SelectHierarchyPath` to return bool (true only when it commits) and take an optional `string? descendantId = null` that it applies through `row.TrySelectDescendant` immediately before `CommitSelection`, so the rejection paths of #614 leave the descendant highlight untouched; make `SelectRow` clear the row's descendant selection before `CommitSelection`. Existing callers in `ActivateSegment`, `ActivateChild`, and the arrow child path compile unchanged. Acceptance: `DescendantActivate_OutsideArchiveRoot_LeavesSelectionAndHighlightUnchanged` and the unchanged `BreadcrumbBridgeRouterIssue614Tests` class pass in P3-T10. +- [x] [P3-T7] In `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` `ExpandLeafAsync`, immediately after the provider await and before any mutation, log and return when `FindRow(row.RowId)` is not the same instance as `row`; otherwise call `row.SetLeafDescendants(children)` in place of `SetLeafChildren`, still mapping the children for the unchanged `subfolderResult` message, then toggle and post as today, with no `ConfigureAwait`. Acceptance: `ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow`, `ExpandLeaf_CurrentRow_StillPostsSubfolderResultThenRender`, `ExpandLeaf_KeyedChildren_RenderNodeAffordances`, and the unchanged `BreadcrumbBridgeRouterTests` and `BreadcrumbBridgeRouterQueueTests` classes pass in P3-T10. +- [x] [P3-T8] Create `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` (registered in `QuickFiler/QuickFiler.csproj`) with `HandleDescendantToggleAsync(BreadcrumbRow, string)` (collapse and post one row render when `TryCollapseDescendant` succeeds; otherwise log and return when `TryBeginDescendantExpand` fails; otherwise await `GetImmediateSubfoldersAsync(key, CancellationToken.None)` without `ConfigureAwait`, then attach and post one row render only when the row is the same instance and `TryAttachDescendantChildren` accepts; on cancellation or a provider fault log, call `TryAbandonDescendantExpand` on the same row instance, and post nothing; never post `subfolderResult` or touch the selection) and `ActivateDescendant(BreadcrumbRow, string)` (log and return when `GetDescendantPath` is null; otherwise `SelectHierarchyPath(row, path, nodeId)`). Acceptance: the eleven router tests and the five `DescendantFetch_` tests pass in P3-T10. +- [x] [P3-T9] In `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` `ProcessInboundAsync`, add one case arm per new type that delegates to `HandleDescendantToggleAsync` or `ActivateDescendant` with `message.NodeId!` (null-forgiving, because the codec guarantees a non-empty value for both types, following the existing `message.Key!` arm) and then breaks; add no other logic. Acceptance: the file stays at or below 500 physical lines and P3-T10 passes. +- [x] [P3-T10] Rebuild with ANALYZE, then run SCOPED-TEST with the class filter `FullyQualifiedName~Breadcrumb` over both test assemblies (covering the five new Group A classes and every existing breadcrumb suite the spec keeps green); write `/evidence/regression-testing/efc-descendants-pass-after..md`. Acceptance: `0 Error(s)`, `EXIT_CODE: 0`, zero failed, and every Group A fail-before test listed as Passed. + +### Phase 4 — Group B Seam and Regression Tests (fail first) + +- [x] [P4-T1] Declaration-only seam: in `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` add an optional trailing `string? preferredRowText = null` to the internal `BindRowsAsync` (with its XML `param`) and call `SelectPreferredRow(preferredRowText)` immediately before its `DeliverDocument()`; declare `private void SelectPreferredRow(string? preferredRowText)` in `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` with a body that selects nothing; in `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` give `BindBreadcrumbRowsAsync` an optional `string preferredRowText = null` (no `?` annotation: that file carries no `#nullable enable`, so an annotation raises CS8632, which TYPECHECK promotes to an error) passed through to the router. Rebuild with ANALYZE; write `/evidence/other/seam-build-efc-trash..md`. Acceptance: `0 Error(s)` with every existing caller of both bind methods unchanged. +- [x] [P4-T2] [expect-fail] Create `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs` (registered) with the six trash router tests the spec names, capturing `NavigateToString` documents and every `SelectedFolderPathChanged` notification. Acceptance: the class compiles in P4-T4 and holds all six names. +- [x] [P4-T3] [expect-fail] Create `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` (registered) as a new partial of `EfcFormControllerTests` with the four controller tests the spec names, wiring a real router over strict mocks into the controller as the existing `Issue439BindBreadcrumbRowsAsync_SubmitsArchiveRootToRealRouter` test does; neither existing `EfcFormControllerTests` file is edited. Acceptance: the class compiles in P4-T4 and holds all four names. +- [x] [P4-T4] [expect-fail] Rebuild with ANALYZE, then run SCOPED-TEST over `BreadcrumbBridgeRouterTrashIssue1002Tests` and the `EfcFormControllerTests` class; write `/evidence/regression-testing/efc-trash-fail-before..md` with `ExpectedExitCode: 1`. Acceptance: every test in the Group B fail-before set is reported Failed with its message recorded, `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows` is reported Passed, and every other new Group B test is listed with its outcome. + +### Phase 5 — Group B Fix (EFC trash preselection) + +- [x] [P5-T1] In `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`, extract the selection-value computation of `SelectRow` (banner refusal, trash text or filing target, #614 archive-root normalization and its two rejection logs) into `TryGetRowSelection(BreadcrumbRow, out string)` used by `SelectRow`, and implement `SelectPreferredRow`: return when the text is null or empty or no non-banner row presents it (trash pseudo-row presents `BreadcrumbRowBuilder.TrashRowText`, a suggestion presents its `FilingTarget`, ordinal comparison) or `TryGetRowSelection` refuses; otherwise set `_selectedRowId` and `SelectedFolderPath` and raise `SelectedFolderPathChanged` once, posting no outbound message. Acceptance: the six trash router tests and the unchanged `BreadcrumbBridgeRouterIssue614Tests` and `BreadcrumbBridgeRouterTests` classes pass in P5-T3. +- [x] [P5-T2] In `QuickFiler/Controllers/EfcFormController.Actions.cs`, replace `ApplyDeleteGesture` with `ApplyDeleteGestureAsync`, which assigns `_folderRows = WithTrashRow(_folderRows)`, returns when `_formViewer` or `_router` is null, and otherwise awaits `BindBreadcrumbRowsAsync(_folderRows, TrashRowText)`; `ActionDeleteAsync` keeps awaiting the viewer UI context first and then awaits the gesture, with no `ConfigureAwait(false)`. Acceptance: the four controller tests, `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows`, and `AsyncVoidBoundary_WhenFaulted_LogsOnceAndDoesNotThrow` pass in P5-T3. +- [x] [P5-T3] Rebuild with ANALYZE, then run SCOPED-TEST with the filter `FullyQualifiedName~Breadcrumb|FullyQualifiedName~EfcFormController|FullyQualifiedName~EfcHomeController` over both test assemblies; write `/evidence/regression-testing/efc-trash-pass-after..md`. Acceptance: `0 Error(s)`, `EXIT_CODE: 0`, zero failed, and every Group B fail-before test listed as Passed. + +### Phase 6 — Group C Regression Tests (fail first) + +- [x] [P6-T1] [expect-fail] Create `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` (registered) with the four spec-named tests over `FolderBreadcrumbDocument.Html`, slicing the `makeSubfolders` body up to `function render` with a private helper in the new file: `subfolderElement.appendChild(name)` occurs before `subfolderElement.appendChild(toggle)`; the recursive call matches `makeSubfolders(row, child.children, selectedChildOwnsActive, depth + 1)` allowing whitespace; the body carries `"data-depth"`, `padding-left: `, and `24 + 16 * depth`; the pinned prefix `makeSubfolders(row, child.children` is present and the only functions declared inside the body are `toggleDescendant` and `activateSubfolder`. Acceptance: the class compiles in P6-T3 and holds all four names. +- [x] [P6-T2] [expect-fail] Create `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs` using `tests/quickfiler-breadcrumb/issue-997-browser-harness.mjs` and `tests/quickfiler-breadcrumb/issue-997-source-loader.mjs` over the real `FolderBreadcrumb.js`, with a three-level expanded descendant payload and the two register tests: in every `.sub` that holds a toggle, the toggle's child index exceeds the name's; `.sub` rows at depths zero, one, and two carry `data-depth` values 0, 1, and 2 and padding-left values 24px, 40px, and 56px. Acceptance: the file runs under the node test runner and holds both names. +- [x] [P6-T3] [expect-fail] Rebuild with ANALYZE (the script is an embedded resource, so the rebuild is required), run SCOPED-TEST over `FolderBreadcrumbNestingIssue1002Tests`, and run `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs` with the node test runner; write `/evidence/regression-testing/qfc-nesting-fail-before..md` with `ExpectedExitCode: 1`. Acceptance: every Group C fail-before test is reported failing with its message recorded, and `MakeSubfolders_PinnedRecursiveCallPrefixPreserved` is listed with its outcome. + +### Phase 7 — Group C Fix (QFC nested affordance order and per-depth indent) + +- [x] [P7-T1] In `QuickFiler/Resources/FolderBreadcrumb.js` `makeSubfolders` only, add a trailing `depth` parameter treated as zero when absent, set `subfolderElement.setAttribute("data-depth", String(depth))` and `subfolderElement.setAttribute("style", "padding-left: " + (24 + 16 * depth) + "px")`, move `subfolderElement.appendChild(name)` ahead of the `if (child.hasChildren)` block so the toggle is appended after the name, and change the recursive call to `makeSubfolders(row, child.children, selectedChildOwnsActive, depth + 1)`. `render`, `makeRow`, the message listener, `FolderBreadcrumb.html`, and `Resources.resx` are not edited, and no function is added. Acceptance: P7-T2 passes. +- [x] [P7-T2] Rebuild with ANALYZE, run SCOPED-TEST over `FolderBreadcrumbNestingIssue1002Tests`, `FolderBreadcrumbAssetContractTests`, and `FolderBreadcrumbRecursiveAssetIssue997Tests`, and run `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs`; write `/evidence/regression-testing/qfc-nesting-pass-after..md`. Acceptance: `0 Error(s)`, zero failed in both runs, and every Group C fail-before test listed as Passed. +- [x] [P7-T3] Run the three files `tests/quickfiler-breadcrumb/issue-997-common.test.mjs`, `issue-997-recursive.test.mjs`, and `issue-1002-nesting.test.mjs` in one node process three times (normal order, reversed order, and a seeded shuffled order) with experimental coverage limited to `QuickFiler/Resources/FolderBreadcrumb.js`; write `/evidence/regression-testing/node-suites-run..md` recording each file order, the seed, the pass and fail counts, and the line coverage of `FolderBreadcrumb.js` against the P0-T9 figure. Acceptance: zero failures in all three orders. + +### Phase 8 — Scope, Policy, Isolation, and Manual-Check Gates + +- [x] [P8-T1] Write `/evidence/qa-gates/scope-fences..md` from `git diff --name-status` against the merge base of `origin/main` and `HEAD` plus `git status --porcelain`: every listed path is a Write Set file or under `/`, none is a file the spec excludes, and the only project files are the four in the Write Set; a search of `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs` and `QuickFiler/Controllers/EfcFormController*.cs` for `FolderBreadcrumbBridgeRouter` and `FolderBreadcrumbDocument` returns zero, with a positive control on `QuickFiler/Viewers/BreadcrumbBridgeCoordinator.cs` returning at least one; and the merge-base diff of the router files and `BreadcrumbDocumentAssets.cs` adds no line containing `ArrowLeft`, `ArrowRight`, `ArrowUp`, `ArrowDown`, or `HandleArrowKeyAsync`. Acceptance: every check in this task holds as stated. +- [x] [P8-T2] Write `/evidence/qa-gates/thread-affinity..md`: a search of `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs` for `ConfigureAwait(false)` and for `lock (` returns zero, `ConfigureAwait` is absent from `BreadcrumbBridgeRouter.Descendants.cs`, and the merge-base diff of `QuickFiler/Controllers/EfcFormController.Actions.cs` adds no line containing `ConfigureAwait(false)`; positive control: the same `ConfigureAwait(false)` search over `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.Descendants.cs` returns at least one. Acceptance: every check holds. +- [x] [P8-T3] Write `/evidence/qa-gates/file-sizes..md` with the physical line count of every new or modified C# file and `QuickFiler/Resources/FolderBreadcrumb.js` beside its P0-T7 value, the merge-base diff of `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` (two case arms and the preferred-row parameter, its documentation, and its call only), and confirmation that none of the seven near-limit test files appears in the P8-T1 path list. Acceptance: every count is at most 500. +- [x] [P8-T4] Write `/evidence/qa-gates/test-policy-audit..md` with one row per new C# test file under `UtilitiesCS.Test/` and `QuickFiler.Test/`: MSTest, Moq, and FluentAssertions usings; `MockBehavior.Strict` on every mock; Arrange-Act-Assert structure; zero matches for `Path.GetTemp`, `File.Write`, `File.Create`, `Thread.Sleep`, `Task.Delay`, and `new WebView2BreadcrumbHost`; and the `` entry in its project file. Acceptance: every row passes. +- [x] [P8-T5] Run each of the eight new C# test classes in its own fresh SCOPED-TEST process in reverse order of the spec's Test Strategy list, then all eight in one process; write `/evidence/regression-testing/csharp-isolation..md` with the class order and per-run counts. Acceptance: zero failures in every run. +- [x] [P8-T6] Write `/evidence/other/manual-outlook-check..md` with `Status: PENDING HUMAN INTERACTION`, the EFC and QFC verification steps from the spec Test Strategy, the reason it cannot be automated (live Outlook, mailbox, WebView2 runtime), and the statement that no agent executed it. Acceptance: the artifact carries all four elements and claims no execution. + +### Phase 9 — Final QA Loop, AC Check-Off, and Self-Review + +Loop rule: P9-T1 to P9-T4 form one toolchain pass. If any of them fails or changes a file, fix the cause and restart at P9-T1; a fresh artifact is written for each step of the pass that is recorded as clean. Editing `spec.md` checkboxes and writing Markdown evidence changes no toolchain input and does not restart the loop. + +- [x] [P9-T1] Capture `git status --porcelain`, run `dotnet tool run csharpier format .`, capture `git status --porcelain` again, then run `dotnet tool run csharpier check .`; write `/evidence/qa-gates/final-toolchain-format..md` with both status captures. Acceptance: the two captures are identical (format rewrote nothing) and the check exits 0 with a line beginning `Checked `; a rewrite of any file restarts the loop, and a rewrite of a file outside the Write Set halts the plan. +- [x] [P9-T2] Run ANALYZE on `TaskMaster.sln`; write `/evidence/qa-gates/final-toolchain-analyzers..md`. Acceptance: `EXIT_CODE: 0`, `0 Error(s)`, and no warning whose path is a Write Set file. +- [x] [P9-T3] Run TYPECHECK on `TaskMaster.sln`; write `/evidence/qa-gates/final-toolchain-typecheck..md`. Acceptance: `EXIT_CODE: 0` and `0 Error(s)`. +- [x] [P9-T4] Run TEST-COVERAGE through `scripts/vscode/Invoke-MSTestWithCoverage.ps1` (shell-icon carve-out applies); write `/evidence/qa-gates/final-toolchain-tests..md` with the counts, the one-line first-party coverage summary, and a list derived from the trx. Acceptance: zero failed; every new test name from the spec Test Strategy and the Test Name Register appears as Passed; every existing class the spec lists as kept green appears with zero failures. +- [x] [P9-T5] Write `/evidence/qa-gates/coverage-comparison..md` from the P0-T6 and P9-T4 Cobertura outputs: baseline and final repository line and branch percentages against the 80 and 75 percent floors; line coverage of `BreadcrumbRow.Descendants.cs` and `BreadcrumbBridgeRouter.Descendants.cs` and of each new method in modified files (`SetLeafDescendants`, `HandleDescendantToggleAsync`, `ActivateDescendant`, `TryGetRowSelection`, `SelectPreferredRow`, `ApplyDeleteGestureAsync`); changed-line coverage per modified production file, computed as the covered share of the added or modified lines in the merge-base diff that Cobertura instruments; and a search of `coverage.config` and the diff showing no exclusion added. Acceptance: each new file and new method is at least 90 percent, no changed-line figure is below the baseline coverage of the lines it replaced, and the final repository figures are not below the baseline figures; a shortfall is recorded as remediation-required, not PASS. +- [x] [P9-T6] Write `/evidence/qa-gates/evidence-form..md` from `git diff --name-status` against the merge base of `origin/main` and `HEAD` plus `git status --porcelain`: no added path ends in `.trx`, `.coverage`, or `.xml`, or contains `cobertura` or `jacoco`; every file under `/evidence/` is Markdown; and a search of `/evidence/` for a drive-rooted path or a user-profile segment returns zero. Acceptance: every check holds. +- [x] [P9-T7] Check off AC1 in `/spec.md` citing P3-T10 (`BreadcrumbMessageCodecIssue1002Tests`, `BreadcrumbMessageCodecTests`). Acceptance: only the AC1 box changes. +- [x] [P9-T8] Check off AC2 in `/spec.md` citing P3-T10 and P8-T1 (`BreadcrumbRowDescendantsIssue1002Tests`, `BreadcrumbRowStateTests`, `BreadcrumbDescendantNode.cs` absent from the diff). Acceptance: only the AC2 box changes. +- [x] [P9-T9] Check off AC3 in `/spec.md` citing P3-T10 (`ExpandLeaf_KeyedChildren_RenderNodeAffordances`, `ExpandLeaf_CurrentRow_StillPostsSubfolderResultThenRender`). Acceptance: only the AC3 box changes. +- [x] [P9-T10] Check off AC4 in `/spec.md` citing P3-T10 (`DescendantToggle_SecondLevelNode_FetchesWithNodeKeyAndRendersChildren`, `DescendantToggle_ExpandedNode_CollapsesWithoutProviderCall`, `DescendantToggle_DoesNotChangeSelection`, `DescendantToggle_PostsNoSubfolderResult`). Acceptance: only the AC4 box changes. +- [x] [P9-T11] Check off AC5 in `/spec.md` citing P3-T10 (`DescendantActivate_Grandchild_CommitsArchiveRelativeStem`, `DescendantActivate_ThenRowConfirmed_RaisesConfirmationOnce`, `DescendantActivate_OutsideArchiveRoot_LeavesSelectionAndHighlightUnchanged`). Acceptance: only the AC5 box changes. +- [x] [P9-T12] Check off AC6 in `/spec.md` citing P3-T10 (`DescendantRequest_UnknownNode_IsRejectedWithoutStateChange`, `DescendantToggle_LeafNode_IsRejectedWithoutProviderCall`, `DescendantRequest_NodeFromAnotherRow_IsRejected`). Acceptance: only the AC6 box changes. +- [x] [P9-T13] Check off AC7 in `/spec.md` citing P3-T10 (the five `DescendantFetch_` tests). Acceptance: only the AC7 box changes. +- [x] [P9-T14] Check off AC8 in `/spec.md` citing P3-T10 (`ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow`, `ExpandLeaf_CurrentRow_StillPostsSubfolderResultThenRender`). Acceptance: only the AC8 box changes. +- [x] [P9-T15] Check off AC9 in `/spec.md` citing P3-T10 (the six `RenderRowFragment_` tests and `BreadcrumbHtmlRendererTests`). Acceptance: only the AC9 box changes. +- [x] [P9-T16] Check off AC10 in `/spec.md` citing P3-T10 (the three `BridgeJs_` tests and `BaseCss_Child_IsFlexRow`). Acceptance: only the AC10 box changes. +- [x] [P9-T17] Check off AC11 in `/spec.md` citing P5-T3 (the six `BindRows_` tests). Acceptance: only the AC11 box changes. +- [x] [P9-T18] Check off AC12 in `/spec.md` citing P5-T3 and P9-T4 (`BindRows_PreferredRowAbsent_SelectsNothing`, `BindRows_NoPreferredRow_KeepsExistingClearBehavior`, existing bind callers green). Acceptance: only the AC12 box changes. +- [x] [P9-T19] Check off AC13 in `/spec.md` citing P5-T3 (the four `ActionDeleteAsync_` tests in `EfcFormControllerTests.Issue1002.cs` and `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows`). Acceptance: only the AC13 box changes. +- [x] [P9-T20] Check off AC14 in `/spec.md` citing P7-T2 (`MakeSubfolders_AppendsNameBeforeToggle`). Acceptance: only the AC14 box changes. +- [x] [P9-T21] Check off AC15 in `/spec.md` citing P7-T2 and P8-T1 (the depth tests, `FolderBreadcrumbAssetContractTests`, `FolderBreadcrumbRecursiveAssetIssue997Tests`, `FolderBreadcrumb.html` absent from the diff). Acceptance: only the AC15 box changes. +- [x] [P9-T22] Check off AC16 in `/spec.md` citing P7-T3 (`/evidence/regression-testing/node-suites-run..md`). Acceptance: only the AC16 box changes. +- [x] [P9-T23] Check off AC17 in `/spec.md` citing the six fail-before and pass-after artifacts under `/evidence/regression-testing/`. Acceptance: only the AC17 box changes. +- [x] [P9-T24] Check off AC18 in `/spec.md` citing `/evidence/qa-gates/scope-fences..md`. Acceptance: only the AC18 box changes. +- [x] [P9-T25] Check off AC19 in `/spec.md` citing `/evidence/qa-gates/thread-affinity..md`. Acceptance: only the AC19 box changes. +- [x] [P9-T26] Check off AC20 in `/spec.md` citing `/evidence/qa-gates/file-sizes..md`. Acceptance: only the AC20 box changes. +- [x] [P9-T27] Check off AC21 in `/spec.md` citing `/evidence/qa-gates/test-policy-audit..md` and P9-T4. Acceptance: only the AC21 box changes. +- [x] [P9-T28] Check off AC22 in `/spec.md` citing `/evidence/qa-gates/final-toolchain-tests..md` and P8-T1. Acceptance: only the AC22 box changes. +- [x] [P9-T29] Check off AC23 in `/spec.md` citing `/evidence/qa-gates/coverage-comparison..md`, only when that artifact records no shortfall. Acceptance: only the AC23 box changes. +- [x] [P9-T30] Check off AC24 in `/spec.md` citing the four `final-toolchain-` artifacts under `/evidence/qa-gates/` from one clean pass. Acceptance: only the AC24 box changes. +- [x] [P9-T31] Check off AC25 in `/spec.md` citing `/evidence/qa-gates/evidence-form..md`. Acceptance: only the AC25 box changes. +- [x] [P9-T32] Check off AC26 in `/spec.md` citing `/evidence/other/manual-outlook-check..md`. Acceptance: only the AC26 box changes. +- [x] [P9-T33] Reconcile `/spec.md` `## Acceptance Criteria`: count checked and unchecked AC boxes and write `/evidence/qa-gates/ac-reconciliation..md` with the acceptance-criteria-tracking status summary. Acceptance: 26 checked and 0 unchecked, with every criterion text unchanged, verified by comparing each AC line, with its checkbox marker normalized, against the same line of `spec.md` at `HEAD` (`spec.md` does not exist at the merge base, so a merge-base comparison cannot observe the criterion text). +- [x] [P9-T34] Walk the policy-audit, code-review, and feature-audit template checklists (resolved per `.claude/skills/policy-audit-template-usage/SKILL.md`; `docs/features/templates/policy_audit/` is absent in TaskMaster, and an unresolvable template is recorded as a deviation with the skill's canonical section list used instead) against the diff from the merge base of `origin/main` and `HEAD`; write `/evidence/qa-gates/self-review..md` with one PASS, FAIL, or N/A row per checklist item. Acceptance: no FAIL row; a FAIL is fixed, the loop re-runs from P9-T1, and the self-review is rewritten. + +## Planner Internal Review Record + +PLANNER-INTERNAL-REVIEW: PASS +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.cs | ProcessInboundAsync +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.cs | BindRowsAsync +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | SelectRow +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | SelectHierarchyPath +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | CommitSelection +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | DeliverDocument +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs | FindRow +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs | ExpandLeafAsync +CITATION: QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs | HandleLeafToggleAsync +CITATION: QuickFiler/Controllers/EfcFormController.Actions.cs | ApplyDeleteGesture +CITATION: QuickFiler/Controllers/EfcFormController.Actions.cs | WithTrashRow +CITATION: QuickFiler/Controllers/EfcFormController.Breadcrumb.cs | BindBreadcrumbRowsAsync +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs | ActivateSegment +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs | CollapseAfter +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs | LeftArrow +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs | SetLeafChildren +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbDescendantNode.cs | BreadcrumbDescendantNode +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs | BreadcrumbInboundMessage +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs | IsKnownInboundType +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs | AppendChildren +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs | BridgeJs +CITATION: UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbSegment.cs | FolderPath +CITATION: UtilitiesCS/OutlookObjects/Folder/BreadcrumbRowBuilder.cs | MapSegments +CITATION: QuickFiler/Resources/FolderBreadcrumb.js | makeSubfolders +CITATION: QuickFiler.Test/Viewers/FolderBreadcrumbAssetContractTests.cs | FunctionBody +CITATION: UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererTests.cs | Issue995BridgeJsPostsConfirmationForPlainEnterAndScrollsSelectedRowIntoView +CITATION: QuickFiler.Test/Controllers/EfcFormControllerTests.cs | ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows +CITATION: QuickFiler.Test/Controllers/EfcFormControllerTests.cs | AsyncVoidBoundary_WhenFaulted_LogsOnceAndDoesNotThrow +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.ps1 | Invoke-MSTestWithCoverageMain +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9, AC10, AC11, AC12, AC13, AC14, AC15, AC16, AC17, AC18, AC19, AC20, AC21, AC22, AC23, AC24, AC25, AC26 +AC-MAPPING: AC1 | IMPLEMENTATION: P1-T1, P3-T1 | TESTS: BreadcrumbMessageCodecIssue1002Tests | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC2 | IMPLEMENTATION: P1-T2, P3-T2, P3-T3 | TESTS: BreadcrumbRowDescendantsIssue1002Tests | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC3 | IMPLEMENTATION: P3-T7 | TESTS: ExpandLeaf_KeyedChildren_RenderNodeAffordances | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC4 | IMPLEMENTATION: P3-T8, P3-T9 | TESTS: DescendantToggle_SecondLevelNode_FetchesWithNodeKeyAndRendersChildren | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC5 | IMPLEMENTATION: P3-T6, P3-T8 | TESTS: DescendantActivate_Grandchild_CommitsArchiveRelativeStem | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC6 | IMPLEMENTATION: P3-T8 | TESTS: DescendantRequest_UnknownNode_IsRejectedWithoutStateChange | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC7 | IMPLEMENTATION: P3-T2, P3-T8 | TESTS: BreadcrumbBridgeRouterStaleCompletionIssue1002Tests | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC8 | IMPLEMENTATION: P3-T7 | TESTS: ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC9 | IMPLEMENTATION: P3-T4 | TESTS: BreadcrumbHtmlRendererIssue1002Tests | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC10 | IMPLEMENTATION: P3-T5 | TESTS: BridgeJs_NodeAffordanceBranch_PrecedesChildAndLeafBranches | EVIDENCE: efc-descendants-pass-after +AC-MAPPING: AC11 | IMPLEMENTATION: P4-T1, P5-T1 | TESTS: BreadcrumbBridgeRouterTrashIssue1002Tests | EVIDENCE: efc-trash-pass-after +AC-MAPPING: AC12 | IMPLEMENTATION: P4-T1, P5-T1 | TESTS: BindRows_NoPreferredRow_KeepsExistingClearBehavior | EVIDENCE: efc-trash-pass-after +AC-MAPPING: AC13 | IMPLEMENTATION: P4-T1, P5-T2 | TESTS: EfcFormControllerTests.Issue1002 | EVIDENCE: efc-trash-pass-after +AC-MAPPING: AC14 | IMPLEMENTATION: P7-T1 | TESTS: MakeSubfolders_AppendsNameBeforeToggle | EVIDENCE: qfc-nesting-pass-after +AC-MAPPING: AC15 | IMPLEMENTATION: P7-T1 | TESTS: MakeSubfolders_SetsDataDepthAndPerDepthPadding | EVIDENCE: qfc-nesting-pass-after +AC-MAPPING: AC16 | IMPLEMENTATION: P6-T2, P7-T1 | TESTS: issue-1002-nesting.test.mjs | EVIDENCE: node-suites-run +AC-MAPPING: AC17 | IMPLEMENTATION: P2-T6, P3-T10, P4-T4, P5-T3, P6-T3, P7-T2 | TESTS: Group A, B, and C fail-before sets | EVIDENCE: efc-descendants-fail-before +AC-MAPPING: AC18 | IMPLEMENTATION: P8-T1 | TESTS: scope-fence searches with positive control | EVIDENCE: scope-fences +AC-MAPPING: AC19 | IMPLEMENTATION: P3-T8, P5-T2, P8-T2 | TESTS: thread-affinity searches with positive control | EVIDENCE: thread-affinity +AC-MAPPING: AC20 | IMPLEMENTATION: P8-T3 | TESTS: physical line counts | EVIDENCE: file-sizes +AC-MAPPING: AC21 | IMPLEMENTATION: P8-T4, P9-T4 | TESTS: test-policy audit rows | EVIDENCE: test-policy-audit +AC-MAPPING: AC22 | IMPLEMENTATION: P9-T4 | TESTS: kept-green existing classes | EVIDENCE: final-toolchain-tests +AC-MAPPING: AC23 | IMPLEMENTATION: P0-T6, P9-T5 | TESTS: TEST-COVERAGE runs | EVIDENCE: coverage-comparison +AC-MAPPING: AC24 | IMPLEMENTATION: P9-T1, P9-T2, P9-T3, P9-T4 | TESTS: full toolchain pass | EVIDENCE: final-toolchain-tests +AC-MAPPING: AC25 | IMPLEMENTATION: P9-T6 | TESTS: evidence-form checks | EVIDENCE: evidence-form +AC-MAPPING: AC26 | IMPLEMENTATION: P8-T6 | TESTS: manual check record review | EVIDENCE: manual-outlook-check +UNRESOLVED-GAPS: NONE +PREFLIGHT: REVISIONS REQUIRED diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T10-39.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T10-39.md new file mode 100644 index 000000000..d0dc6050d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T10-39.md @@ -0,0 +1,237 @@ +# Policy Audit: EFC and QFC folder tree selection and nesting (Issue #1002) + +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002` +- Head: `e96e65302de00d3b0f8174736c90a59f82525dc9` +- Base: `main` (resolved `origin/main`) at merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` +- Review timestamp: 2026-10-22T10-39 (initial review; no prior remediation cycle) +- Work mode: `full-bug` (AC source: `spec.md` only) +- Reviewer: feature-review agent + +## Template Provenance + +The structure of this artifact follows the canonical major headings listed in `.claude/skills/policy-audit-template-usage/SKILL.md` step 5 (Executive Summary, sections 1 to 10, Appendix A and Appendix B); the template-asset selector step was not run from this agent session. The artifact is validated with `validate_orchestration_artifacts` (artifact type `policy-audit`). + +## Rejected Scope Narrowing + +No scope narrowing was detected in the caller prompt. The caller supplied the base branch, the merge base, the feature folder, the AC source and the canonical C# coverage artifact. `git merge-base main HEAD` returns `8e4ad1ecb`, equal to the supplied value, and the PR-context summary names head `e96e65302`, equal to `git rev-parse HEAD`, so it is current. The audit scope is the full branch diff `8e4ad1ecb...e96e65302` (75 files). + +## Executive Summary + +The branch fixes three QuickFiler defects: (1) EFC gains a keyed descendant tree, identity-addressed `descendantToggle` and `descendantActivate` bridge messages, recursive rendering and stale-completion guards; (2) the EFC trash gesture awaits a bind that preselects the trash row inside the delivered document; (3) the QFC `makeSubfolders` script appends the name before the toggle and indents each level by 16px. + +Overall verdict: **REMEDIATION_REQUIRED (4 blocking findings: 3 autonomous, 1 human_decision_required)**. + +- Toolchain: PASS (format, analyzer rebuild, warnings-as-errors rebuild, MSTest with coverage; loop pass 3 evidence, 7579 of 7579 tests passed). +- Repository C# coverage: PASS (86.14% line, 80.37% branch; floors 80% line and 75% branch from root CLAUDE.md). +- New and changed C# code: PASS (292 of 292 instrumented changed lines covered; both new files 100% line). +- Modified-file C# coverage: FAIL for three modified files below the governing floors (B-1, B-2, B-3), one of which was lowered by this change (B-1). +- Code review: one blocking functional gap (B-4: the selected EFC descendant has no visual highlight). +- AC status: 26 of 26 PASS. + +## 1. General Unit Test Policy Compliance + +### 1.1 Core principles + +| Principle | Verdict | Evidence | +|---|---|---| +| Independence | PASS | Each test class builds its fixture in `[TestInitialize]` (`EfcDescendantRouterFixture`); no shared static state; no real `WebView2BreadcrumbHost` (0 references in new test files). | +| Isolation | PASS | One behaviour per test; codec, row, renderer, router, controller and asset-contract units are tested in separate files. | +| Fast execution | PASS | No sleeps or delays; full suite completed in the Koverage run. | +| Determinism | PASS | Ordering is driven by `TaskCompletionSource` barriers (5 in the stale-completion file); 0 `Thread.Sleep` and 0 `Task.Delay` in added lines. | +| Readability | PASS | Descriptive names from the spec Test Strategy; Arrange/Act/Assert comments; FluentAssertions `because` texts. | +| No temporary files | PASS | 0 `Path.GetTemp`, `GetTempFileName`, `File.Write` or `File.Create` in added lines. | + +### 1.2 Coverage requirements + +Coverage thresholds applied: the root `CLAUDE.md` states C# line `>= 80%` and branch `>= 75%`, and new modules, classes and methods `>= 90%`; these govern under the threshold precedence of `.claude/rules/general-unit-test.md`. The canonical artifact `artifacts/csharp/coverage.xml` is byte-identical (`cmp`) to the executor's final post-processed Cobertura and holds exactly nine first-party packages, so its root attributes are the first-party figures. `quality-tiers.yml` is absent at the repository root and on the base branch, so tier classification is not a finding. + +**Coverage Metrics by Language:** + +| Language | Files Changed | Tests | Test Result | Baseline Coverage | Post-Change Coverage | New Code Coverage | +|---|---|---|---|---|---|---| +| C# | 12 production (2 new, 10 modified), 8 test, 4 project files | MSTest | 7579 passed, 0 failed | 86.10% | 86.14% | 100% | +| JavaScript | 1 production asset, 1 node test | node --test | 80 passed, 0 failed | 100% | 100% | 100% | +| TypeScript | 0 | none | none | N/A | N/A | N/A | +| Python | 0 | none | none | N/A | N/A | N/A | +| PowerShell | 0 | none | none | N/A | N/A | N/A | + +### Coverage Evidence Checklist + +- C# baseline coverage artifact: `evidence/baseline/baseline-coverage.2026-10-22T09-54.md` (first-party summary line; the post-processed Cobertura was kept in the session scratchpad and re-read by the reviewer) +- C# post-change coverage artifact: `artifacts/csharp/coverage.xml` with summary `evidence/qa-gates/final-toolchain-tests.2026-10-22T10-26.md` +- JavaScript baseline coverage artifact: `evidence/baseline/baseline-node-suites.2026-10-22T09-53.md` +- JavaScript post-change coverage artifact: `evidence/regression-testing/node-suites-run.2026-10-22T10-14.md` +- TypeScript baseline coverage artifact: `N/A - out of scope` +- TypeScript post-change coverage artifact: `N/A - out of scope` +- PowerShell baseline coverage artifact: `N/A - out of scope` +- PowerShell post-change coverage artifact: `N/A - out of scope` +- Python baseline coverage artifact: `N/A - out of scope` +- Python post-change coverage artifact: `N/A - out of scope` +- Per-language comparison summary: section 1.2.1 of this document + +### 1.2.1 Per-Language Coverage Comparison + +- C#: Baseline: 86.10% lines (57272/66515) / 80.31% branches (13923/17337) -> Post-change: 86.14% lines (57523/66780) / 80.37% branches (14016/17439). Change: +0.04% lines, +0.06% branches. New/changed-code coverage: 100%. Disposition: FAIL. Evidence: evidence/qa-gates/coverage-comparison.2026-10-22T10-26.md; artifacts/csharp/coverage.xml; reviewer per-file union over class line nodes (section 5). +- JavaScript: Baseline: 100% lines (FolderBreadcrumb.js) -> Post-change: 100% lines (FolderBreadcrumb.js). Change: 0.00% lines. New/changed-code coverage: 100%. Disposition: PASS. Evidence: evidence/baseline/baseline-node-suites.2026-10-22T09-53.md; evidence/regression-testing/node-suites-run.2026-10-22T10-14.md. +- TypeScript: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero TypeScript files changed on this branch. +- Python: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero Python files changed on this branch. +- PowerShell: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero PowerShell files changed on this branch. + +### 1.2.2 Coverage Verdicts + +C# repo-wide coverage verdict: PASS (86.14% line at or above 80, 80.37% branch at or above 75; both rose against the baseline). + +C# new-code coverage verdict: PASS (`BreadcrumbRow.Descendants.cs` 100.00% line 82/82, 97.22% branch 35/36; `BreadcrumbBridgeRouter.Descendants.cs` 100.00% line 63/63, 91.67% branch 11/12; every new method at least 90% line). + +C# changed-line coverage verdict: PASS (292 of 292 instrumented changed lines covered across the ten modified production files; no changed line lost coverage). + +C# modified-file coverage verdict: FAIL (three modified files are below the 80% line or 75% branch floor: `EfcFormController.Breadcrumb.cs` 64.46% line / 52.38% branch, lowered from 67.83% / 57.14% by this change (B-1); `BreadcrumbMessages.cs` 100.00% line / 50.00% branch, unchanged from baseline (B-2); `EfcFormController.Actions.cs` 23.42% line / 33.33% branch, raised from 21.30% / 28.12% (B-3)). + +JavaScript coverage verdict: PASS (node `--experimental-test-coverage` over `QuickFiler/Resources/FolderBreadcrumb.js`: 100% line, branch and function before and after, in normal, reversed and shuffled suite order). CI runs no node suite; the CI-visible contract for the script is the C# text-contract file `FolderBreadcrumbNestingIssue1002Tests.cs`. + +### 1.3 Scenario completeness + +PASS. Positive (toggle, activate, preferred trash row), negative (unknown row, unknown node, cross-row node, leaf toggle, empty or non-string `nodeId`), edge (out-of-root and archive-root activation, preferred row absent, host core not initialized, null router), error (provider fault, cancellation, bind fault at the controller boundary), concurrency (collapse, rebind and reverse completion during an awaited fetch) and state (generation counter, reset points) scenarios are each pinned by a named test. + +## 2. General Code Change Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| Design: simplicity and separation of concerns | PASS | Tree state lives on `BreadcrumbRow` (pure model); I/O-bound fetch and guards live in a new router partial; rendering stays in the renderer. | +| Reuse | PASS | `BreadcrumbDescendantNode` reused unchanged; `TryGetRowSelection` extracted and shared by `SelectRow` and `SelectPreferredRow`. | +| Error handling | PASS | Rejected identities, stale completions, faults and cancellation are logged and leave state unchanged; `OnHostMessageReceived` still catches only `BreadcrumbMessageException`. | +| Logging | PASS | Existing `log.Error` router pattern. | +| File size (500 lines) | PASS | Largest changed source file `BreadcrumbBridgeRouter.cs` 471 lines; largest new test file 487 lines; `.csproj` files are project files. | +| Public contracts documented | PASS | XML documentation on every new public member and the new optional parameters. | +| Dependencies | PASS | None added. | +| Host-identifier hygiene in committed files | PASS | Added-line sweep for the account name, its 8.3 short form, the encoded scratchpad prefix, the user e-mail and drive-rooted user or program paths: 0 hits. | +| Bugfix workflow (failing test first) | PASS | Fail-before projections for all three defects (`efc-descendants-fail-before`, `efc-trash-fail-before`, `qfc-nesting-fail-before`) precede the pass-after projections. | +| Functional completeness of the fix | FAIL | B-4: the selected EFC descendant carries `aria-selected="true"` but no stylesheet rule renders it, so the user cannot see which subfolder is the filing destination (see code review). | + +## 3. Language-Specific Code Change Policy Compliance + +### 3.1 C# (`CLAUDE.md` C# Code Change Policy, `.claude/rules/csharp.md`) + +| Rule | Verdict | Evidence | +|---|---|---| +| CSharpier formatting | PASS | `evidence/qa-gates/final-toolchain-format.2026-10-22T10-23.md`: `csharpier check .` exit 0, 0 files changed by the format pass. | +| Analyzer rebuild | PASS | `evidence/qa-gates/final-toolchain-analyzers.2026-10-22T10-23.md`: 0 errors, 0 warnings. | +| Nullable rebuild (warnings as errors) | PASS | `evidence/qa-gates/final-toolchain-typecheck.2026-10-22T10-23.md`: 0 errors, 0 warnings; no `/p:Nullable=enable`; new files carry `#nullable enable`. | +| Null-forgiving operator justified | PASS | `key!` in `BreadcrumbBridgeRouter.Descendants.cs` carries a comment explaining that net48 lacks `NotNullWhen`. | +| Thread affinity (spec I6, AC19) | PASS | 0 `ConfigureAwait(false)` and 0 `lock (` in `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs` (reviewer grep). | +| Public API compatibility | PASS | New parameters are optional (`nodeId`, `preferredRowText`); existing callers compile unchanged. | +| Suppressions | PASS | 0 `#pragma`, `SuppressMessage` or `ExcludeFromCodeCoverage` in added lines. | + +### 3.2 JavaScript (`QuickFiler/Resources/FolderBreadcrumb.js`) + +| Rule | Verdict | Evidence | +|---|---|---| +| Edit confined to `makeSubfolders` | PASS | Diff touches only `makeSubfolders`; pinned strings preserved (FolderBreadcrumbAssetContractTests 15 of 15, FolderBreadcrumbRecursiveAssetIssue997Tests 3 of 3). | +| File size | PASS | 350 lines. | + +## 4. Language-Specific Unit Test Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| MSTest attributes | PASS | `[TestClass]` and `[TestMethod]` in all eight new C# test files. | +| Moq strict mocks | PASS | Every `new Mock<` in new files uses `MockBehavior.Strict` (6 of 6); router suites share the strict fixture. | +| FluentAssertions | PASS | 221 `Should()` assertions; 0 MSTest `Assert.` calls. | +| Compile registration | PASS | Explicit `Compile Include` for all new source and test files in the four legacy project files. | +| Test location | PASS | Sibling `.Test` projects mirroring production folders, as CLAUDE.md and every existing C# test do; node suite under `tests/quickfiler-breadcrumb/`. | + +## 5. Test Coverage Detail + +Per-file figures are the reviewer's union of `//` nodes per `filename` in `artifacts/csharp/coverage.xml` (head) and the executor's baseline Cobertura; they reproduce the executor's figures exactly. + +| File | Status | Baseline line / branch | Post-change line / branch | Gate | +|---|---|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` | new | absent | 100.00% (82/82) / 97.22% (35/36) | new code at least 90%: PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | new | absent | 100.00% (63/63) / 91.67% (11/12) | new code at least 90%: PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | modified | 98.71% / 92.39% | 98.77% / 92.55% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | modified | 100.00% / 50.00% (7/14) | 100.00% / 50.00% (7/14) | branch below 75%: FAIL (B-2) | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | modified | 100.00% / 100.00% | 100.00% / 100.00% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | modified | 96.99% / 90.00% | 97.77% / 93.10% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | modified | no executable lines | no executable lines | const strings only; no class node emitted | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | modified | 99.17% / 95.00% | 99.19% / 95.00% | PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | modified | 96.24% / 90.38% | 96.40% / 89.29% | PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | modified | 98.04% / 96.15% | 98.37% / 95.59% | PASS | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | modified | 21.30% (23/108) / 28.12% (9/32) | 23.42% (26/111) / 33.33% (12/36) | below 80% / 75%: FAIL (B-3) | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | modified | 67.83% (78/115) / 57.14% (24/42) | 64.46% (78/121) / 52.38% (22/42) | below floor and lowered: FAIL (B-1, B-3) | + +Changed-line detail: every instrumented added line in the ten modified files is covered (BreadcrumbRow.cs 8/8, BreadcrumbMessages.cs 3/3, BreadcrumbMessageCodec.cs 15/15, BreadcrumbHtmlRenderer.cs 49/49, BreadcrumbBridgeRouter.cs 5/5, .Arrows.cs 7/7, .Selection.cs 48/48, EfcFormController.Actions.cs 5/5, EfcFormController.Breadcrumb.cs 7/7). The only partially covered new branch is line 102 of `EfcFormController.Actions.cs` (3 of 4 conditions; the `_formViewer == null` arm is unreached). + +The `EfcFormController.Breadcrumb.cs` drop is caused by the diff: the delete gesture previously reached `BindFolderRows` (6 of its 7 lines covered by `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows`); it now awaits `BindBreadcrumbRowsAsync` directly, and the remaining caller `BindSourceFolderRows` has no test. Covered branches fell from 24 to 22. + +## 6. Test Execution Metrics + +- MSTest full suite with coverage (executor loop pass 3): 7579 total, 7579 passed, 0 failed (`evidence/qa-gates/final-toolchain-tests.2026-10-22T10-26.md`). +- New test classes: 75 results, 0 not passed; kept-green existing classes listed in Test Strategy: 0 failures (same evidence). +- Fail-before runs: `evidence/regression-testing/efc-descendants-fail-before.2026-10-22T10-04.md`, `efc-trash-fail-before.2026-10-22T10-10.md`, `qfc-nesting-fail-before.2026-10-22T10-13.md` (expected exit 1). +- Pass-after runs: `efc-descendants-pass-after.2026-10-22T10-08.md`, `efc-trash-pass-after.2026-10-22T10-12.md`, `qfc-nesting-pass-after.2026-10-22T10-14.md` (exit 0). +- Node suites (local, not run by CI): 80 of 80 in each of three orders (`evidence/regression-testing/node-suites-run.2026-10-22T10-14.md`). +- Reviewer did not re-run the suites; the reviewer re-derived every coverage figure from the canonical artifact, which is byte-identical to the executor's final Cobertura. + +## 7. Code Quality Checks + +| Check | Command | Result | Verdict | +|---|---|---|---| +| Confidentiality masking scan | `git diff 8e4ad1ecb...HEAD` added lines, case-insensitive match for the account name, its 8.3 short form, the encoded scratchpad prefix, the user e-mail and a drive-rooted user or program path regex | 0 hits | PASS | +| Suppression scan (added lines) | match for `SuppressMessage`, `#pragma`, `ExcludeFromCodeCoverage` in code | 0 in code (document mentions only) | PASS | +| Workflow change scan | `git diff --name-only 8e4ad1ecb...HEAD` filtered for `.github/workflows`, `.github/actions`, `scripts/benchmarks` | empty; rule `modified-workflow-needs-green-run` not triggered | PASS | +| Scope fence scan | `git diff --name-only` filtered for the spec's excluded files | empty | PASS | +| Raw evidence scan | `git diff --name-only` filtered for `.trx`, `.coverage`, `.xml`, `cobertura` | empty | PASS | + +## Evidence Location Compliance + +`git diff --name-only 8e4ad1ecb...HEAD` filtered for `^artifacts/(baselines|qa|evidence|coverage)/` is empty, so no file is committed under a non-canonical evidence path. All committed evidence is under `/evidence/{baseline,qa-gates,regression-testing,other}/` as Markdown projections. The script `validate_evidence_locations.py` is not present in this checkout, so the check was performed by diff inspection. Verdict: PASS. + +## 8. Gaps and Exceptions + +- B-1 (Blocking, autonomous): `EfcFormController.Breadcrumb.cs` coverage lowered by the change and below the floor. +- B-2 (Blocking, autonomous): `BreadcrumbMessages.cs` branch coverage 50.00%, below the 75% floor; the uncovered branches are the null-argument guards of four constructors, including the constructor this diff modified. +- B-3 (Blocking, human_decision_required): pre-existing sub-floor residue in `EfcFormController.Actions.cs` and `EfcFormController.Breadcrumb.cs` sits in `MessageBox.Show`-bound action handlers and WebView2 wiring; reaching the floor needs either new seams outside the bug scope or the maintainer-ratified COM/VSTO/WinForms exemption in CLAUDE.md UT2. +- B-4 (Blocking, autonomous): no visual highlight for the selected EFC descendant. +- O-1 (non-blocking): the PR-context summary classifies every changed C# file as docs/tooling ("Core logic changes: 0 files"); language scope was derived from `git diff`. +- O-2 (non-blocking): `remediation-handoff-atomic-planner` prescribes `audit//` and `remediation//` folders, while the review-artifact hook requires the flat `..md` form used here; `feature-review-workflow` step 8 asks the reviewer to create the plan file, while the handoff skill assigns plan authorship to `atomic-planner`. This review follows the hook and the handoff skill. +- O-3 (non-blocking): the manual Outlook check is pending human interaction by design (AC26). + +## 9. Summary of Changes + +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` (new) and `BreadcrumbRow.cs`: keyed descendant tree, generation-checked attach, reset points. +- `BreadcrumbMessages.cs`, `BreadcrumbMessageCodec.cs`: two inbound types and an optional `NodeId`. +- `BreadcrumbHtmlRenderer.cs`, `BreadcrumbDocumentAssets.cs`: recursive keyed rendering, node affordance, bridge click and keydown branches, flex child rule. +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` (new), `BreadcrumbBridgeRouter.cs`, `.Arrows.cs`, `.Selection.cs`: descendant handlers, first-level stale guard, preferred-row preselection. +- `EfcFormController.Actions.cs`, `EfcFormController.Breadcrumb.cs`: awaited delete gesture with the trash row preferred. +- `QuickFiler/Resources/FolderBreadcrumb.js`: name before toggle, per-depth indent. +- Eight new C# test files, one node suite, four project-file registrations; feature documents, evidence and two task-researcher memory notes. + +## 10. Compliance Verdict + +**REMEDIATION_REQUIRED.** Toolchain, repository coverage, new-code coverage, changed-line coverage, scope fences and all 26 acceptance criteria pass. Four blocking findings remain: B-1, B-2 and B-4 are autonomous; B-3 requires a maintainer decision. + +## Appendix A: Test Inventory + +| Test file | Tests | Status | Scope | +|---|---|---|---| +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` | 8 results (5 methods) | new | codec contract | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` | 24 | new | row tree state | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` | 10 | new | renderer and bridge script contract | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` | 11 | new | descendant toggle and activation | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs` | 7 | new | stale-completion guards | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs` | 7 | new | preferred-row bind | +| `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | 4 | new | delete gesture end to end | +| `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` | 4 | new | QFC script text contract | +| `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs` | 2 | new, local only | QFC DOM behaviour | + +## Appendix B: Toolchain Commands Reference + +| Stage | Command | Run by | +|---|---|---| +| C# format | `dotnet tool run csharpier format .` then `dotnet tool run csharpier check .` | executor | +| C# analyzers | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | executor | +| C# type check | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | executor | +| C# test and coverage | `pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` | executor | +| Node suites | `node --test --test-isolation=none --test-concurrency=1 --experimental-test-coverage --test-coverage-include=QuickFiler\Resources\FolderBreadcrumb.js` over the three suites | executor | +| Diff scope | `git diff --numstat 8e4ad1ecb...HEAD`; `git merge-base main HEAD` | reviewer | +| Coverage parsing | Python `xml.etree.ElementTree` union of class line nodes per filename plus `git diff -U0` new-side hunks, over `artifacts/csharp/coverage.xml` and the executor baseline Cobertura; `cmp` of the canonical and executor final XML | reviewer | +| Scans | `git diff 8e4ad1ecb...HEAD` added-line filters for identity tokens, suppressions, sleeps, temp files, `ConfigureAwait(false)`; `grep` of router files for `lock (` | reviewer | diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T13-37.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T13-37.md new file mode 100644 index 000000000..c488d6e11 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T13-37.md @@ -0,0 +1,244 @@ +# Policy Audit: EFC and QFC folder tree selection and nesting (Issue #1002) + +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002` +- Head: `b72ac1dbbfdb6b45d01a14dde9e91428a6bf41ac` +- Base: `main` (resolved `origin/main`) at merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` +- Review timestamp: 2026-10-22T13-37 (re-audit after remediation cycle 1; prior review 2026-10-22T10-39) +- Work mode: `full-bug` (AC source: `spec.md` only) +- Reviewer: feature-review agent + +## Template Provenance + +The structure of this artifact follows the canonical major headings listed in `.claude/skills/policy-audit-template-usage/SKILL.md` step 5 (Executive Summary, sections 1 to 10, Appendix A and Appendix B), in the same shape as the cycle-0 audit `policy-audit.2026-10-22T10-39.md`; the template-asset selector step was not run from this agent session. The artifact is validated with `validate_orchestration_artifacts` (artifact type `policy-audit`). + +## Rejected Scope Narrowing + +No scope narrowing was detected in the caller prompt. The caller supplied the base branch, the merge base, the feature folder, the AC source, the canonical C# coverage artifact and the prior remediation inputs. `git merge-base main HEAD` and `git merge-base origin/main HEAD` both return `8e4ad1ecb`, equal to the supplied value, and the PR-context summary names head `b72ac1dbb`, equal to `git rev-parse HEAD`, so it is current. The audit scope is the full branch diff `8e4ad1ecb...b72ac1dbb` (105 files), not only the cycle-1 commit. + +## Executive Summary + +The branch fixes three QuickFiler defects (EFC keyed descendant tree with identity-addressed bridge messages; EFC trash gesture that preselects the trash row inside the bound document; QFC nested-row affordance order and per-depth indent). Remediation cycle 1 (commit `b72ac1dbb`) addressed all four cycle-0 blocking findings: B-1 and B-2 by new tests, B-3 by the maintainer-ratified member-level `[ExcludeFromCodeCoverage]` exemption, and B-4 by `.child[aria-selected="true"]` theme rules; it also applied the recommended CR-1 and CR-4 items. + +Overall verdict: **REMEDIATION_REQUIRED (1 blocking finding: 1 autonomous)**. + +- Toolchain: PASS (cycle-1 pass: format, format check, analyzer rebuild, warnings-as-errors rebuild, MSTest with coverage; 7597 of 7597 tests passed; reviewer read the trx counters). +- Repository C# coverage: PASS (86.30% line, 80.58% branch; floors 80% line and 75% branch from root CLAUDE.md). +- New and changed C# code: PASS (both new files 100% line; every instrumented changed line covered). +- Modified-file C# coverage: PASS (every modified production file at or above 80% line and 75% branch; reviewer recomputation). +- Cycle-0 findings B-1, B-2, B-3, B-4: resolved (section 8). +- New blocking finding R-1: spec AC23 states "no coverage exclusion is added", which the ratified B-3 exemption made false while the criterion remains checked; the spec text was not amended to record the maintainer decision. + +## 1. General Unit Test Policy Compliance + +### 1.1 Core principles + +| Principle | Verdict | Evidence | +|---|---|---| +| Independence | PASS | Each cycle-1 test builds its own controller through `CreateIssue1002Controller`; no shared static state; message-constructor tests are pure. | +| Isolation | PASS | One behaviour per test (null viewer, null router, null rows, bind path; one constructor argument per null-guard test; one CSS rule per contract test). | +| Fast execution | PASS | No sleeps or delays; full suite completed in the coverage run. | +| Determinism | PASS | The four async bind tests await a `TaskCompletionSource` set by the host mock's `NavigateToString` callback; 0 `Thread.Sleep`, `Task.Delay` or temp-file APIs in added lines (reviewer scan). Code-review N-3 records that these waits have no failure exit if a bind faults. | +| Readability | PASS | Descriptive names; Arrange/Act/Assert comments; FluentAssertions `because` texts. | +| No temporary files | PASS | 0 `Path.GetTemp`, `GetTempFileName`, `File.Write` or `File.Create` in added lines. | + +### 1.2 Coverage requirements + +Coverage thresholds applied: the root `CLAUDE.md` states C# line `>= 80%` and branch `>= 75%`, and new modules, classes and methods `>= 90%`; these govern under the threshold precedence of `.claude/rules/general-unit-test.md`. The canonical artifact `artifacts/csharp/coverage.xml` (written 13:31Z) is byte-identical (`cmp`) to `coverage/coverage.cobertura.xml` from the cycle-1 run, whose trx (`coverage/test-results/mstest-coverage-run.trx`, 13:30Z) reports 7597 passed; every cycle-1 source edit predates the run (latest 13:27Z), and the commit `b72ac1dbb` (13:33Z) adds no further source change. Root attributes `lines-covered="57541" lines-valid="66677" branches-covered="14036" branches-valid="17419"` are the first-party totals. `quality-tiers.yml` is absent at the repository root and on `origin/main`, so tier classification is not a finding. + +**Coverage Metrics by Language:** + +| Language | Files Changed | Tests | Test Result | Baseline Coverage | Post-Change Coverage | New Code Coverage | +|---|---|---|---|---|---|---| +| C# | 12 production (2 new, 10 modified), 9 test, 4 project files | MSTest | 7597 passed, 0 failed | 86.10% | 86.30% | 100% | +| JavaScript | 1 production asset, 1 node test | node --test | 80 passed, 0 failed | 100% | 100% | 100% | +| TypeScript | 0 | none | none | N/A | N/A | N/A | +| Python | 0 | none | none | N/A | N/A | N/A | +| PowerShell | 0 | none | none | N/A | N/A | N/A | + +### Coverage Evidence Checklist + +- C# baseline coverage artifact: `evidence/baseline/baseline-coverage.2026-10-22T09-54.md` (first-party summary line; the post-processed baseline Cobertura was kept in the session scratchpad and re-read by the reviewer) +- C# post-change coverage artifact: `artifacts/csharp/coverage.xml` with summaries `evidence/qa-gates/remediation-tests.2026-10-22T13-32.md` and `evidence/qa-gates/coverage-comparison.2026-10-22T13-33.md` +- JavaScript baseline coverage artifact: `evidence/baseline/baseline-node-suites.2026-10-22T09-53.md` +- JavaScript post-change coverage artifact: `evidence/regression-testing/node-suites-run.2026-10-22T10-14.md` (cycle 1 did not change `FolderBreadcrumb.js`) +- TypeScript baseline coverage artifact: `N/A - out of scope` +- TypeScript post-change coverage artifact: `N/A - out of scope` +- PowerShell baseline coverage artifact: `N/A - out of scope` +- PowerShell post-change coverage artifact: `N/A - out of scope` +- Python baseline coverage artifact: `N/A - out of scope` +- Python post-change coverage artifact: `N/A - out of scope` +- Per-language comparison summary: section 1.2.1 of this document + +### 1.2.1 Per-Language Coverage Comparison + +- C#: Baseline: 86.10% lines (57272/66515) / 80.31% branches (13923/17337) -> Post-change: 86.30% lines (57541/66677) / 80.58% branches (14036/17419). Change: +0.20% lines, +0.27% branches. New/changed-code coverage: 100%. Disposition: PASS. Evidence: evidence/qa-gates/coverage-comparison.2026-10-22T13-33.md; artifacts/csharp/coverage.xml; reviewer per-file union over class line nodes (section 5). +- JavaScript: Baseline: 100% lines (FolderBreadcrumb.js) -> Post-change: 100% lines (FolderBreadcrumb.js). Change: 0.00% lines. New/changed-code coverage: 100%. Disposition: PASS. Evidence: evidence/baseline/baseline-node-suites.2026-10-22T09-53.md; evidence/regression-testing/node-suites-run.2026-10-22T10-14.md. +- TypeScript: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero TypeScript files changed on this branch. +- Python: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero Python files changed on this branch. +- PowerShell: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero PowerShell files changed on this branch. + +### 1.2.2 Coverage Verdicts + +C# repo-wide coverage verdict: PASS (86.30% line at or above 80, 80.58% branch at or above 75; both rose against the baseline). + +C# new-code coverage verdict: PASS (`BreadcrumbRow.Descendants.cs` 100.00% line 82/82, 97.22% branch 35/36; `BreadcrumbBridgeRouter.Descendants.cs` 100.00% line 63/63, 91.67% branch 11/12). + +C# changed-line coverage verdict: PASS (every instrumented changed line in the ten modified production files is covered; reviewer `git diff -U0` new-side hunks intersected with Cobertura line nodes). One changed line is no longer instrumented: the formatter-reordered `public async Task ActionOkAsync()` signature, now inside a ratified exempt member; it carries no behaviour change. + +C# modified-file coverage verdict: PASS (every modified production file at or above 80% line and 75% branch; `EfcFormController.Breadcrumb.cs` 92.08% / 80.95%, `EfcFormController.Actions.cs` 92.86% / 81.25%, `BreadcrumbMessages.cs` 100.00% / 100.00%; the two controller partials reach the floor with the ratified member-level exemption in force). + +JavaScript coverage verdict: PASS (node `--experimental-test-coverage` over `QuickFiler/Resources/FolderBreadcrumb.js`: 100% line, branch and function before and after; unchanged by cycle 1). CI runs no node suite; the CI-visible contract for the script is the C# text-contract file `FolderBreadcrumbNestingIssue1002Tests.cs`. + +### 1.3 Scenario completeness + +PASS. Cycle 1 added the missing negative and edge scenarios named by the cycle-0 findings: null viewer, null router and null rows for `BindSourceFolderRows` and `BindFolderRows`; the null-viewer arm of `ApplyDeleteGestureAsync`; each null constructor argument of `BreadcrumbInboundMessage`, `BreadcrumbOutboundMessage`, `BreadcrumbRenderMessage` and `BreadcrumbSubfolderResultMessage`; and the light, dark and base CSS contracts for the descendant highlight and name truncation. + +## 2. General Code Change Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| Design: simplicity and separation of concerns | PASS | Cycle 1 production change is limited to two CSS constant lines, one CSS rule split, five attributes with comments and two `using` directives. | +| Reuse | PASS | Cycle-1 tests reuse the existing `CreateIssue1002Controller`, `SetPrivateField` and `GetPrivateField` helpers. | +| Error handling | PASS | No production error path changed in cycle 1. | +| Logging | PASS | No change. | +| File size (500 lines) | PASS | Reviewer `awk` counts: largest test file 487 (`BreadcrumbBridgeRouterIssue1002Tests.cs`), largest production file 471 (`BreadcrumbBridgeRouter.cs`); cycle-1 files 313, 98, 343, 182, 206, 219. | +| Public contracts documented | PASS | No public signature changed in cycle 1 other than the formatter modifier reorder on `ActionOkAsync`. | +| Dependencies | PASS | None added. | +| Host-identifier hygiene in committed files | PASS | Added-line sweep for the account name, its 8.3 short form, the encoded scratchpad prefix, the user e-mail, and drive-rooted or `Program Files` / `AppData` paths: 0 hits (three GitHub issue URLs only). | +| Bugfix workflow (failing test first) | PASS | B-4 and CR-1 fail-before projection `regression-testing/css-highlight-fail-before.2026-10-22T13-27.md` (3 expected failures) precedes `css-highlight-pass-after.2026-10-22T13-28.md`. | +| Requirements traceability | FAIL | R-1: spec AC23 asserts "no coverage exclusion is added", but the diff adds five `[ExcludeFromCodeCoverage]` attributes; the maintainer ratification is recorded only in `remediation-inputs.2026-10-22T10-39.md`, and AC23 remains checked with unamended text. | + +## 3. Language-Specific Code Change Policy Compliance + +### 3.1 C# (`CLAUDE.md` C# Code Change Policy, `.claude/rules/csharp.md`) + +| Rule | Verdict | Evidence | +|---|---|---| +| CSharpier formatting | PASS | `evidence/qa-gates/remediation-format.2026-10-22T13-32.md` (no tracked change on the last format run) and `remediation-format-check.2026-10-22T13-32.md` (`Checked 1675 files`, exit 0). | +| Analyzer rebuild | PASS | `evidence/qa-gates/remediation-analyzers.2026-10-22T13-32.md`: `/t:Rebuild`, 0 warnings, 0 errors. | +| Nullable rebuild (warnings as errors) | PASS | `evidence/qa-gates/remediation-typecheck.2026-10-22T13-32.md`: `/t:Rebuild`, 0 warnings, 0 errors, no `/p:Nullable=enable`; the new UtilitiesCS test file carries `#nullable enable`. | +| Coverage exemption form | PASS | Five member-level `[ExcludeFromCodeCoverage]` attributes on exactly the members the maintainer named (`ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `RefreshSuggestionsAsync`, `ConfigureBreadcrumbControl`); no class-level attribute; `git diff --stat 8e4ad1ecb...HEAD -- coverage.config` empty. The `Justification` property requested by the decision does not exist on the .NET Framework 4.8.1 attribute (CS0246 probe, `evidence/other/exclude-attribute-form.2026-10-22T13-25.md`), so each attribute carries an adjacent comment naming the dependency and #1002. CLAUDE.md UT2 names this mechanism; under `policy-compliance-order` CLAUDE.md precedes `.claude/rules/general-unit-test.md`, whose Blocking clause addresses coverage-config `exclude` entries. | +| `RefreshSuggestionsAsync` conditional scope | PASS | The decision admitted it only if uncovered after the B-1 tests; `evidence/qa-gates/b3-refresh-suggestions-decision.2026-10-22T13-24.md` records 0 of 9 lines covered after those tests. | +| Thread affinity (spec I6, AC19) | PASS | 0 `ConfigureAwait(false)` and 0 `lock (` in `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs` (reviewer grep). | +| Public API compatibility | PASS | No signature change in cycle 1. | +| Suppressions | PASS | 0 `#pragma` or `SuppressMessage` in added lines; the five `ExcludeFromCodeCoverage` lines are the ratified exemption above. | + +### 3.2 JavaScript and CSS assets + +| Rule | Verdict | Evidence | +|---|---|---| +| `FolderBreadcrumb.js` edit confined to `makeSubfolders` | PASS | Unchanged by cycle 1; cycle-0 evidence stands. | +| EFC CSS constants | PASS | `.child[aria-selected="true"]` added to `LightThemeCss` (`#cce8ff`) and `DarkThemeCss` (`#264f78`), matching the row selection colours; truncation moved to `.child .name`. | +| File size | PASS | `FolderBreadcrumb.js` 350 lines; `BreadcrumbDocumentAssets.cs` 182 lines. | + +## 4. Language-Specific Unit Test Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| MSTest attributes | PASS | `[TestClass]` and `[TestMethod]` in all nine new C# test files, including `BreadcrumbMessagesIssue1002Tests.cs`. | +| Moq strict mocks | PASS | Cycle-1 controller tests use the strict host and provider mocks of `CreateIssue1002Controller`; the message tests need no mocks. | +| FluentAssertions | PASS | 0 MSTest `Assert.` calls in added lines. | +| Compile registration | PASS | `UtilitiesCS.Test.csproj` gains `Compile Include="OutlookObjects\Folder\BreadcrumbMessagesIssue1002Tests.cs"`. | +| Test location | PASS | Sibling `.Test` projects mirroring production folders, as CLAUDE.md and every existing C# test do. | + +## 5. Test Coverage Detail + +Per-file figures are the reviewer's union of `//` nodes per `filename` in `artifacts/csharp/coverage.xml` (head) and the executor's baseline Cobertura, with changed lines from `git diff -U0 8e4ad1ecb...HEAD`; they reproduce the executor's `b1`, `b2`, `b3` and `coverage-comparison` figures exactly. + +| File | Status | Baseline line / branch | Post-change line / branch | Gate | +|---|---|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` | new | absent | 100.00% (82/82) / 97.22% (35/36) | new code at least 90%: PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | new | absent | 100.00% (63/63) / 91.67% (11/12) | new code at least 90%: PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | modified | 98.71% / 92.39% | 98.77% / 92.55% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | modified | 100.00% / 50.00% (7/14) | 100.00% / 100.00% (14/14) | PASS (B-2 resolved) | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | modified | 100.00% / 100.00% | 100.00% / 100.00% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | modified | 96.99% / 90.00% | 97.77% / 93.10% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | modified | no executable lines | no executable lines | const strings only; no class node emitted | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | modified | 99.17% / 95.00% | 99.19% / 95.00% | PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | modified | 96.24% / 90.38% | 96.40% / 89.29% | PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | modified | 98.04% / 96.15% | 98.37% / 95.59% | PASS | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | modified | 21.30% (23/108) / 28.12% (9/32) | 92.86% (26/28) / 81.25% (13/16) | PASS (B-3 resolved by ratified exemption) | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | modified | 67.83% (78/115) / 57.14% (24/42) | 92.08% (93/101) / 80.95% (34/42) | PASS (B-1 resolved: 93 covered lines and 34 covered branches exceed the baseline 78 and 24) | + +Changed-line detail: instrumented changed lines covered per modified file: BreadcrumbRow.cs 8/8, BreadcrumbMessages.cs 3/3, BreadcrumbMessageCodec.cs 15/15, BreadcrumbHtmlRenderer.cs 49/49, BreadcrumbBridgeRouter.cs 5/5, .Arrows.cs 7/7, .Selection.cs 48/48, EfcFormController.Actions.cs 5/5, EfcFormController.Breadcrumb.cs 7/7. The `_formViewer == null` arm of `ApplyDeleteGestureAsync` (cycle-0 CR-4) is now exercised by `ApplyDeleteGestureAsync_NullViewer_RetainsTrashRowAndDoesNotBind`, raising the file's covered branches from 12 to 13. + +Exemption consequence: the valid-line counts of the two controller partials fell (Actions 111 to 28; Breadcrumb 121 to 101) because the five attributed members, including their async state machines and lambda closures, are no longer instrumented; the Cobertura document holds one `QuickFiler.Controllers.EfcFormController` class node per file and no residual generated class for those members. Those members had 0 covered lines at baseline and at the cycle-0 head, so no previously covered line was removed from measurement. + +## 6. Test Execution Metrics + +- MSTest full suite with coverage (cycle-1 pass): 7597 total, 7597 passed, 0 failed; reviewer read `` in `coverage/test-results/mstest-coverage-run.trx` (summary `evidence/qa-gates/remediation-tests.2026-10-22T13-32.md`). +- Cycle-1 tests: 18 new results (8 controller, 7 message-constructor, 3 CSS contract), each `outcome="Passed"` in that trx (reviewer grep by test name). +- Fail-before and pass-after for cycle 1: `evidence/regression-testing/css-highlight-fail-before.2026-10-22T13-27.md` (exit 1, 3 expected failures), `css-highlight-pass-after.2026-10-22T13-28.md`, `efc-bind-tests-pass.2026-10-22T13-22.md`, `breadcrumb-messages-null-guards-pass.2026-10-22T13-26.md`. +- Cycle-0 fail-before and pass-after projections for the three defects remain in `evidence/regression-testing/`. +- Node suites (local, not run by CI): 80 of 80 in each of three orders (`evidence/regression-testing/node-suites-run.2026-10-22T10-14.md`); cycle 1 changed no JavaScript. +- The reviewer did not re-run the suites; every coverage figure above was re-derived from the canonical artifact. + +## 7. Code Quality Checks + +| Check | Command | Result | Verdict | +|---|---|---|---| +| Confidentiality masking scan | `git diff 8e4ad1ecb...HEAD` added lines, case-insensitive match for the account name, its 8.3 short form, the encoded scratchpad prefix, the user e-mail, and a drive-letter, `Program Files` or `AppData` path regex | 0 hits (three GitHub issue URLs matched the drive-letter pattern and were inspected) | PASS | +| Suppression scan (added lines) | match for `SuppressMessage`, `#pragma`, `ExcludeFromCodeCoverage` in `*.cs` | 5 `[ExcludeFromCodeCoverage]`, all on the ratified members; 0 others | PASS | +| Workflow change scan | `git diff --name-only 8e4ad1ecb...HEAD` filtered for `.github/` | empty; rule `modified-workflow-needs-green-run` not triggered | PASS | +| Scope fence scan | `git diff --name-only` filtered for the spec's excluded files | empty | PASS | +| Raw evidence scan | `git diff --name-only` filtered for `.trx`, `.coverage`, `.xml`, `cobertura` | empty | PASS | +| Coverage configuration scan | `git diff --stat 8e4ad1ecb...HEAD -- coverage.config` | empty | PASS | + +## Evidence Location Compliance + +`git diff --name-only 8e4ad1ecb...HEAD` filtered for `^artifacts/(baselines|qa|evidence|coverage)/` is empty, so no file is committed under a non-canonical evidence path. All committed evidence is under `/evidence/{baseline,qa-gates,regression-testing,remediation-baseline,other}/` as Markdown projections. The script `validate_evidence_locations.py` is not present in this checkout, so the check was performed by diff inspection. Verdict: PASS. + +## 8. Gaps and Exceptions + +Cycle-0 findings: + +- B-1 (resolved): `EfcFormController.Breadcrumb.cs` 92.08% line / 80.95% branch; 93 covered lines and 34 covered branches against the required 84 and 24. +- B-2 (resolved): `BreadcrumbMessages.cs` 14/14 branches; lines 84, 85, 124, 144, 173, 174 and 175 each `condition-coverage` 100% (2/2). +- B-3 (resolved): maintainer decision recorded 2026-10-22T10:50Z (option a); five member-level attributes on exactly the named members; both controller partials above the floor; no `coverage.config` change. +- B-4 (resolved): two `aria-selected` rules in `BreadcrumbDocumentAssets.cs` (reviewer read); the renderer emits the nested `.children` container as a sibling after the `.child` node, so the highlight covers only the selected node's line. + +New findings: + +- R-1 (Blocking, autonomous): spec AC23 text ("no coverage exclusion is added") contradicts the ratified B-3 exemption; the criterion remains checked and its text was not amended, so the merged spec would record a criterion that the merged code does not meet as written. +- O-1 (non-blocking): the PR-context summary classifies every changed C# file as docs/tooling ("Core logic changes: 0 files"); language scope was derived from `git diff`. +- O-2 (non-blocking): the decision asked for a `Justification` on each attribute; the property does not exist on net48, and an adjacent comment was used instead (evidenced by the CS0246 probe). +- O-3 (non-blocking): the manual Outlook check remains pending human interaction by design (AC26); step 3 ("confirm it is highlighted") is now satisfiable by the B-4 rules. +- O-4 (non-blocking): skill layout conflicts recorded in cycle 0 (flat review-artifact names versus `audit//` folders; plan authorship) are unchanged; this review follows the hook and the handoff skill. + +## 9. Summary of Changes + +- Cycle 0 (`e96e65302`): keyed descendant tree (`BreadcrumbRow.Descendants.cs`, `BreadcrumbRow.cs`), two inbound message types and `NodeId` (`BreadcrumbMessages.cs`, `BreadcrumbMessageCodec.cs`), recursive rendering and bridge script branches (`BreadcrumbHtmlRenderer.cs`, `BreadcrumbDocumentAssets.cs`), router descendant handlers, stale guards and preferred-row bind (`BreadcrumbBridgeRouter*.cs`), awaited trash-preselecting delete gesture (`EfcFormController.Actions.cs`, `.Breadcrumb.cs`), QFC `makeSubfolders` order and indent (`FolderBreadcrumb.js`); eight C# test files, one node suite. +- Cycle 1 (`b72ac1dbb`): 8 controller tests in `EfcFormControllerTests.Issue1002.cs`; new `BreadcrumbMessagesIssue1002Tests.cs` (7 tests) and its `Compile Include`; 3 CSS contract tests; `.child[aria-selected="true"]` theme rules and `.child .name` truncation rule; five ratified `[ExcludeFromCodeCoverage]` attributes; remediation evidence. + +## 10. Compliance Verdict + +**REMEDIATION_REQUIRED.** Toolchain, repository, new-code, changed-line and modified-file coverage, scope fences and evidence form pass, and all four cycle-0 blocking findings are resolved. One blocking finding remains: R-1, an autonomous spec amendment to AC23 recording the maintainer-ratified exemption. + +## Appendix A: Test Inventory + +| Test file | Tests | Status | Scope | +|---|---|---|---| +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` | 8 results (5 methods) | new | codec contract | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs` | 7 | new (cycle 1) | message constructor null guards | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` | 24 | new | row tree state | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` | 13 | new (3 added in cycle 1) | renderer, bridge script and CSS contract | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` | 11 | new | descendant toggle and activation | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs` | 7 | new | stale-completion guards | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs` | 7 | new | preferred-row bind | +| `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | 12 | new (8 added in cycle 1) | delete gesture, bind helpers | +| `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` | 4 | new | QFC script text contract | +| `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs` | 2 | new, local only | QFC DOM behaviour | + +## Appendix B: Toolchain Commands Reference + +| Stage | Command | Run by | +|---|---|---| +| C# format | `dotnet tool run csharpier format .` then `dotnet tool run csharpier check .` | executor | +| C# analyzers | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | executor | +| C# type check | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | executor | +| C# test and coverage | `pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` | executor | +| Node suites | `node --test --test-isolation=none --test-concurrency=1 --experimental-test-coverage` over the three breadcrumb suites | executor (cycle 0) | +| Diff scope | `git diff --numstat 8e4ad1ecb...HEAD`; `git merge-base main HEAD`; `git merge-base origin/main HEAD` | reviewer | +| Coverage parsing | Python `xml.etree.ElementTree` union of class line nodes per filename plus `git diff -U0` new-side hunks, over `artifacts/csharp/coverage.xml` and the executor baseline Cobertura; `cmp` of the canonical and run Cobertura | reviewer | +| Test outcome check | grep of `coverage/test-results/mstest-coverage-run.trx` for `= 80%` and branch `>= 75%`, and new modules, classes and methods `>= 90%`; these govern under the threshold precedence of `.claude/rules/general-unit-test.md`. The canonical artifact `artifacts/csharp/coverage.xml` (written 14:06:14Z) is the post-processed Cobertura of the cycle-2 run, whose trx (`coverage/test-results/mstest-coverage-run.trx`, 14:05:24Z) reports 7599 passed; every cycle-2 source edit predates the run (latest 14:00:53Z), and commit `54b36112a` (14:07:49Z) adds no further source change. Root attributes `lines-covered="57546" lines-valid="66677" branches-covered="14038" branches-valid="17419"` are the first-party totals. `quality-tiers.yml` is absent at the repository root and on `origin/main`, so tier classification is not a finding. + +**Coverage Metrics by Language:** + +| Language | Files Changed | Tests | Test Result | Baseline Coverage | Post-Change Coverage | New Code Coverage | +|---|---|---|---|---|---|---| +| C# | 12 production (2 new, 10 modified), 9 test, 4 project files | MSTest | 7599 passed, 0 failed | 86.10% | 86.31% | 100% | +| JavaScript | 1 production asset, 1 node test | node --test | 80 passed, 0 failed | 100% | 100% | 100% | +| TypeScript | 0 | none | none | N/A | N/A | N/A | +| Python | 0 | none | none | N/A | N/A | N/A | +| PowerShell | 0 | none | none | N/A | N/A | N/A | + +### Coverage Evidence Checklist + +- C# baseline coverage artifact: `evidence/baseline/baseline-coverage.2026-10-22T09-54.md` (branch baseline) and `evidence/remediation-baseline/cycle2-baseline-tests.2026-10-22T13-54.md` (cycle-2 pre-change) +- C# post-change coverage artifact: `artifacts/csharp/coverage.xml` with summaries `evidence/qa-gates/cycle2-tests.2026-10-22T14-06.md` and `evidence/qa-gates/cycle2-coverage-comparison.2026-10-22T14-06.md` +- JavaScript baseline coverage artifact: `evidence/baseline/baseline-node-suites.2026-10-22T09-53.md` +- JavaScript post-change coverage artifact: `evidence/regression-testing/node-suites-run.2026-10-22T10-14.md` (cycles 1 and 2 did not change `FolderBreadcrumb.js`) +- TypeScript baseline coverage artifact: `N/A - out of scope` +- TypeScript post-change coverage artifact: `N/A - out of scope` +- PowerShell baseline coverage artifact: `N/A - out of scope` +- PowerShell post-change coverage artifact: `N/A - out of scope` +- Python baseline coverage artifact: `N/A - out of scope` +- Python post-change coverage artifact: `N/A - out of scope` +- Per-language comparison summary: section 1.2.1 of this document + +### 1.2.1 Per-Language Coverage Comparison + +- C#: Baseline: 86.10% lines (57272/66515) / 80.31% branches (13923/17337) -> Post-change: 86.31% lines (57546/66677) / 80.59% branches (14038/17419). Change: +0.21% lines, +0.28% branches. New/changed-code coverage: 100%. Disposition: PASS. Evidence: evidence/qa-gates/cycle2-coverage-comparison.2026-10-22T14-06.md; artifacts/csharp/coverage.xml; reviewer per-file union over class line nodes (section 5). +- JavaScript: Baseline: 100% lines (FolderBreadcrumb.js) -> Post-change: 100% lines (FolderBreadcrumb.js). Change: 0.00% lines. New/changed-code coverage: 100%. Disposition: PASS. Evidence: evidence/baseline/baseline-node-suites.2026-10-22T09-53.md; evidence/regression-testing/node-suites-run.2026-10-22T10-14.md. +- TypeScript: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero TypeScript files changed on this branch. +- Python: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero Python files changed on this branch. +- PowerShell: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero PowerShell files changed on this branch. + +### 1.2.2 Coverage Verdicts + +C# repo-wide coverage verdict: PASS (86.31% line at or above 80, 80.59% branch at or above 75; both rose against the branch baseline, and the cycle-2 run gained 4 covered lines against its own pre-change run with branches unchanged). + +C# new-code coverage verdict: PASS (`BreadcrumbRow.Descendants.cs` 100.00% line 82/82, 97.22% branch 35/36; `BreadcrumbBridgeRouter.Descendants.cs` 100.00% line 63/63, 91.67% branch 11/12). + +C# changed-line coverage verdict: PASS (every instrumented changed line in the ten modified production files is covered; reviewer `git diff -U0` new-side hunks intersected with Cobertura line nodes). The cycle-2 production edits are a comment line in `EfcFormController.Actions.cs` and string-constant text in `BreadcrumbDocumentAssets.cs`; neither adds an instrumented line. + +C# modified-file coverage verdict: PASS (every modified production file with instrumented lines at or above 80% line and 75% branch; `EfcFormController.Breadcrumb.cs` 92.08% / 80.95%, `EfcFormController.Actions.cs` 92.86% / 81.25%, both unchanged from cycle 1; `BreadcrumbDocumentAssets.cs` holds only `const` strings and emits no class node). + +JavaScript coverage verdict: PASS (node `--experimental-test-coverage` over `QuickFiler/Resources/FolderBreadcrumb.js`: 100% line, branch and function before and after; unchanged by cycles 1 and 2). CI runs no node suite; the CI-visible contract for the script is the C# text-contract file `FolderBreadcrumbNestingIssue1002Tests.cs`. + +### 1.3 Scenario completeness + +PASS. Cycle 2 adds the negative scenario named by cycle-1 N-3 (a bind that faults before delivery fails the caller's wait) and a text-contract test for the `subfolderResult` name span. Earlier cycles' positive, negative, edge and stale-completion scenarios are unchanged and pass. + +## 2. General Code Change Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| Design: simplicity and separation of concerns | PASS | Cycle-2 production change is one comment line and a four-line replacement inside a JavaScript string constant. | +| Reuse | PASS | The fault exit is wired once in the shared `CreateIssue1002Controller` helper, so all three delivered waits gain it. | +| Error handling | PASS | No production error path changed in cycle 2. | +| Logging | PASS | No change. | +| File size (500 lines) | PASS | Reviewer `awk` counts over every changed `.cs`, `.js` and `.mjs` file: maximum 487 (`BreadcrumbBridgeRouterIssue1002Tests.cs`) and 471 (`BreadcrumbBridgeRouter.cs`); cycle-2 files 339, 368, 185, 206. | +| Public contracts documented | PASS | No public signature changed in cycle 2. | +| Dependencies | PASS | None added. | +| Host-identifier hygiene in committed files | PASS | Added-line sweep for the account name, its 8.3 short form, the encoded scratchpad prefix, the user e-mail, and drive-rooted or `Program Files` / `AppData` paths: the only hits are two prose lines of the cycle-1 audit that name the scan itself; no path or identity value. | +| Bugfix workflow (failing test first) | PASS | `regression-testing/cycle2-delivered-wait-fail-before.2026-10-22T13-58.md` and `cycle2-child-name-fail-before.2026-10-22T14-00.md` each record exactly the one new test failing on assertion before the fix, followed by the matching pass-after projections. | +| Requirements traceability | PASS | R-1 resolved: AC23 names the five ratified members and the maintainer decision, and `spec.md` carries a dated `## Planner Amendments` entry with authority and reason. Reviewer count of added `ExcludeFromCodeCoverage` lines in `*.cs` against the merge base: 5; `git diff --stat 8e4ad1ecb...HEAD -- coverage.config` is empty. | + +## 3. Language-Specific Code Change Policy Compliance + +### 3.1 C# (`CLAUDE.md` C# Code Change Policy, `.claude/rules/csharp.md`) + +| Rule | Verdict | Evidence | +|---|---|---| +| CSharpier formatting | PASS | `evidence/qa-gates/cycle2-format.2026-10-22T14-06.md` (no tracked change beyond the plan file set) and `cycle2-format-check.2026-10-22T14-06.md` (`Checked 1675 files`, exit 0). | +| Analyzer rebuild | PASS | `evidence/qa-gates/cycle2-analyzers.2026-10-22T14-06.md`: `/t:Rebuild`, 0 warnings, 0 errors. | +| Nullable rebuild (warnings as errors) | PASS | `evidence/qa-gates/cycle2-typecheck.2026-10-22T14-06.md`: `/t:Rebuild`, 0 warnings, 0 errors, no `/p:Nullable=enable`. | +| Coverage exemption form | PASS | Five member-level `[ExcludeFromCodeCoverage]` attributes on exactly the maintainer-named members; no class-level attribute; no `coverage.config` change. The `ActionCancelAsync` comment now names the concrete `EfcViewer` dependency (`UiSyncContext` awaiter and `Close`) rather than `MessageBox` (cycle-1 N-1 resolved). CLAUDE.md UT2 names this mechanism; under `policy-compliance-order` CLAUDE.md precedes `.claude/rules/general-unit-test.md`, whose Blocking clause addresses coverage-config `exclude` entries. | +| Thread affinity (spec I6, AC19) | PASS | 0 `ConfigureAwait(false)` and 0 `lock (` in `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs` (reviewer grep; files unchanged in cycle 2). | +| Public API compatibility | PASS | No signature change in cycle 2. | +| Suppressions | PASS | 0 `#pragma` or `SuppressMessage` in added lines; the five `ExcludeFromCodeCoverage` lines are the ratified exemption above. | + +### 3.2 JavaScript and CSS assets + +| Rule | Verdict | Evidence | +|---|---|---| +| `FolderBreadcrumb.js` edit confined to `makeSubfolders` | PASS | Unchanged since cycle 0. | +| EFC bridge script (`BreadcrumbDocumentAssets.BridgeJs`) | PASS | The `subfolderResult` handler now appends a `span.name` holding the display name, so the cycle-1 `.child .name` truncation rule applies to the transient render. Every click branch resolves its target with `closest(...)`, so a click on the inner span still reaches the `.child` branches. | +| AC9 coordinator ruling | PASS | The key-less branch of `BreadcrumbHtmlRenderer` was not edited (no cycle-2 diff to that file); `RenderRowFragment_KeylessChildren_RenderUnchanged` passes with its assertions unchanged. | +| File size | PASS | `FolderBreadcrumb.js` 350 lines; `BreadcrumbDocumentAssets.cs` 185 lines. | + +## 4. Language-Specific Unit Test Policy Compliance + +| Rule | Verdict | Evidence | +|---|---|---| +| MSTest attributes | PASS | `[TestMethod]` on both cycle-2 tests inside existing `[TestClass]` types. | +| Moq strict mocks | PASS | The fault test uses the strict host and provider mocks of `CreateIssue1002Controller`; the text-contract test needs no mocks. | +| FluentAssertions | PASS | 0 MSTest `Assert.` calls in added lines. | +| Compile registration | PASS | No new file in cycle 2; earlier files registered. | +| C# 7.3 compatibility of `QuickFiler.Test` | PASS | No `#nullable enable` or nullable annotations added; builds clean. | +| Test location | PASS | Sibling `.Test` projects mirroring production folders, as CLAUDE.md and every existing C# test do. | + +## 5. Test Coverage Detail + +Per-file figures are the reviewer's union of `//` nodes per `filename` in `artifacts/csharp/coverage.xml` (head), with changed lines from `git diff -U0 8e4ad1ecb...HEAD`; they reproduce the executor's `cycle2-coverage-comparison` figures exactly. + +| File | Status | Baseline line / branch | Post-change line / branch | Gate | +|---|---|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` | new | absent | 100.00% (82/82) / 97.22% (35/36) | new code at least 90%: PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | new | absent | 100.00% (63/63) / 91.67% (11/12) | new code at least 90%: PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | modified | 98.71% / 92.39% | 98.77% / 92.55% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | modified | 100.00% / 50.00% (7/14) | 100.00% / 100.00% (14/14) | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | modified | 100.00% / 100.00% | 100.00% / 100.00% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | modified | 96.99% / 90.00% | 97.77% / 93.10% | PASS | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | modified | no executable lines | no executable lines | const strings only; no class node emitted | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | modified | 99.17% / 95.00% | 99.19% / 95.00% | PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | modified | 96.24% / 90.38% | 96.40% / 89.29% | PASS | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | modified | 98.04% / 96.15% | 98.37% / 95.59% | PASS | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | modified | 21.30% (23/108) / 28.12% (9/32) | 92.86% (26/28) / 81.25% (13/16) | PASS (ratified member-level exemption in force) | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | modified | 67.83% (78/115) / 57.14% (24/42) | 92.08% (93/101) / 80.95% (34/42) | PASS | + +Changed-line detail: instrumented changed lines covered per modified file: BreadcrumbRow.cs 8/8, BreadcrumbMessages.cs 3/3, BreadcrumbMessageCodec.cs 15/15, BreadcrumbHtmlRenderer.cs 49/49, BreadcrumbBridgeRouter.cs 5/5, .Arrows.cs 7/7, .Selection.cs 48/48, EfcFormController.Actions.cs 5/5, EfcFormController.Breadcrumb.cs 7/7. + +## 6. Test Execution Metrics + +- MSTest full suite with coverage (cycle-2 pass): 7599 total, 7599 passed, 0 failed; reviewer read `` in `coverage/test-results/mstest-coverage-run.trx` (summary `evidence/qa-gates/cycle2-tests.2026-10-22T14-06.md`). Cycle-2 pre-change run: 7597 passed. +- Cycle-2 tests: `BindFolderRows_FaultBeforeDelivery_FaultsDeliveredTaskPromptly` and `BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan`, each `outcome="Passed"` in that trx (reviewer grep by test name). +- Fail-before and pass-after for cycle 2: `evidence/regression-testing/cycle2-delivered-wait-fail-before.2026-10-22T13-58.md`, `cycle2-delivered-wait-pass-after.2026-10-22T13-59.md`, `cycle2-child-name-fail-before.2026-10-22T14-00.md`, `cycle2-child-name-pass-after.2026-10-22T14-01.md`. +- Node suites (local, not run by CI): 80 of 80 in each of three orders (`evidence/regression-testing/node-suites-run.2026-10-22T10-14.md`); no JavaScript asset changed since. +- The reviewer did not re-run the suites; every coverage figure above was re-derived from the canonical artifact. + +## 7. Code Quality Checks + +| Check | Command | Result | Verdict | +|---|---|---|---| +| Confidentiality masking scan | `git diff 8e4ad1ecb...HEAD` added lines, case-insensitive match for the account name, its 8.3 short form, the encoded scratchpad prefix, the user e-mail, and a drive-letter, `Program Files` or `AppData` path regex | 0 path or identity values (two prose lines of the cycle-1 audit naming the scan; GitHub issue URLs excluded) | PASS | +| Suppression scan (added lines) | match for `SuppressMessage`, `#pragma`, `ExcludeFromCodeCoverage` in `*.cs` | 5 `[ExcludeFromCodeCoverage]`, all on the ratified members; 0 others | PASS | +| Workflow change scan | `git diff --name-only 8e4ad1ecb...HEAD` filtered for `.github/` | empty; rule `modified-workflow-needs-green-run` not triggered | PASS | +| Scope fence scan | `git diff --name-only` filtered for the spec's excluded files | empty | PASS | +| Raw evidence scan | `git diff --name-only` filtered for `^artifacts/`, `.trx`, `.coverage`, `cobertura` | empty | PASS | +| Coverage configuration scan | `git diff --stat 8e4ad1ecb...HEAD -- coverage.config` | empty | PASS | + +## Evidence Location Compliance + +`git diff --name-only 8e4ad1ecb...HEAD` filtered for `^artifacts/(baselines|qa|evidence|coverage)/` is empty, so no file is committed under a non-canonical evidence path. All committed evidence is under `/evidence/{baseline,qa-gates,regression-testing,remediation-baseline,other}/` as Markdown projections. The script `validate_evidence_locations.py` is not present in this checkout, so the check was performed by diff inspection. Verdict: PASS. + +## 8. Gaps and Exceptions + +Prior findings: + +- B-1 to B-4 (cycle 0): remain resolved; per-file figures unchanged from cycle 1 (section 5). +- R-1 (cycle 1, resolved): AC23 clause replaced with the ratified-exception text; `## Planner Amendments` entry dated 2026-10-22 names the authority (maintainer decision 2026-10-22T10:50Z, B-3) and reason; AC22 and AC24 text unchanged; 5 added attributes; empty `coverage.config` stat. +- N-1 (cycle 1, resolved): comment names `UiSyncContext` and `Close`. +- N-3 (cycle 1, resolved): `BoundaryErrorSink` in `CreateIssue1002Controller` calls `TrySetException` on the delivered source. +- N-5 (cycle 1, resolved as narrowed): `subfolderResult` handler wraps the name; renderer key-less branch deliberately left unchanged under the AC9 ruling recorded in `remediation-plan.2026-10-22T13-43.md`. + +Non-blocking observations: + +- O-1: the PR-context summary classifies every changed C# file as docs/tooling; language scope was derived from `git diff`. +- O-2: net48 `ExcludeFromCodeCoverage` has no `Justification` property; adjacent comments are used. +- O-3: the manual Outlook check stays pending human interaction by design (AC26). +- O-4: remediation-plan task P4-T5 asserted an empty diff of `BreadcrumbHtmlRenderer.cs` against the merge base, which cannot hold because cycles 0 and 1 changed that file; the executor measured against `HEAD` instead and recorded the deviation in `self-review.2026-10-22T14-07.md`. The outcome is correct; the plan wording was imprecise. +- O-5: the cycle-2 self-review records that `docs/features/templates/policy_audit/` is absent in this repository; the executor used the cycle-1 checklists. + +## 9. Summary of Changes + +- Cycle 0 (`e96e65302`): keyed descendant tree, two inbound message types and `NodeId`, recursive rendering and bridge script branches, router descendant handlers, stale guards and preferred-row bind, awaited trash-preselecting delete gesture, QFC `makeSubfolders` order and indent; eight C# test files, one node suite. +- Cycle 1 (`b72ac1dbb`): controller and message-constructor tests, CSS contract tests, `.child[aria-selected="true"]` and `.child .name` rules, five ratified `[ExcludeFromCodeCoverage]` attributes. +- Cycle 2 (`54b36112a` and three docs commits): AC23 amendment and `## Planner Amendments` entry; `ActionCancelAsync` exemption comment; fault exit for the delivered waits plus one new test; `span.name` in the bridge `subfolderResult` handler plus one new test; cycle-2 evidence. + +## 10. Compliance Verdict + +**PASS.** Toolchain, repository, new-code, changed-line and modified-file coverage, scope fences, evidence form and requirements traceability pass. All prior blocking findings (B-1 to B-4, R-1) are resolved, and no new blocking finding was identified. + +## Appendix A: Test Inventory + +| Test file | Tests | Status | Scope | +|---|---|---|---| +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` | 8 results (5 methods) | new | codec contract | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs` | 7 | new | message constructor null guards | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` | 24 | new | row tree state | +| `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` | 14 | new (1 added in cycle 2) | renderer, bridge script and CSS contract | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` | 11 | new | descendant toggle and activation | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs` | 7 | new | stale-completion guards | +| `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs` | 7 | new | preferred-row bind | +| `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` | 13 | new (1 added in cycle 2) | delete gesture, bind helpers, pre-delivery fault | +| `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` | 4 | new | QFC script text contract | +| `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs` | 2 | new, local only | QFC DOM behaviour | + +## Appendix B: Toolchain Commands Reference + +| Stage | Command | Run by | +|---|---|---| +| C# format | `dotnet tool run csharpier format .` then `dotnet tool run csharpier check .` | executor | +| C# analyzers | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | executor | +| C# type check | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | executor | +| C# test and coverage | `pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` | executor | +| Node suites | `node --test --test-isolation=none --test-concurrency=1 --experimental-test-coverage` over the three breadcrumb suites | executor (cycle 0) | +| Diff scope | `git diff --stat 8e4ad1ecb...HEAD`; `git merge-base main HEAD`; `git merge-base origin/main HEAD` | reviewer | +| Coverage parsing | Python `xml.etree.ElementTree` union of class line nodes per filename plus `git diff -U0` new-side hunks, over `artifacts/csharp/coverage.xml` | reviewer | +| Test outcome check | grep of `coverage/test-results/mstest-coverage-run.trx` for `.md`. + +### B-2 BreadcrumbMessages.cs branch coverage below the floor + +- Severity: Blocking +Remediability: autonomous +Remediability-Evidence: the uncovered arms are argument-null guards in public constructors; each is reached by a one-line unit test with no seam or external dependency. +- File: `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs`, constructor lines 84, 85, 124, 144, 173, 174 and 175. +- Problem: branch coverage 50.00% (7/14) at baseline and head, below the 75% branch floor for a modified file; lines 84-85 belong to the `BreadcrumbInboundMessage` constructor this diff modified. +- Expected behaviour after the fix: each `?? throw new ArgumentNullException(...)` arm is exercised; file branch coverage at least 75% (11 of 14 or more). +- Fix: add null-argument tests asserting `ArgumentNullException` with the parameter name for `BreadcrumbInboundMessage` (`type`, `rowId`) and for the guarded parameters of the three outbound message constructors, in a new Issue1002 test file under `UtilitiesCS.Test/OutlookObjects/Folder/` with its `Compile Include`, or in `BreadcrumbMessageCodecIssue1002Tests.cs` (108 lines). +- Verification commands: + - Toolchain in CLAUDE.md order. + - Cobertura `condition-coverage` on lines 84, 85, 124, 144, 173, 174 and 175 of `UtilitiesCS\OutlookObjects\Folder\BreadcrumbMessages.cs`: 100% (2/2) on each. + +### B-3 Pre-existing sub-floor residue in EfcFormController partials + +- Severity: Blocking +Remediability: human_decision_required +Remediability-Evidence: CLAUDE.md UT2 states the COM/VSTO/WinForms coverage exemption must be ratified by the project maintainer; the residue lies in MessageBox-bound action handlers and WebView2 host wiring, so the choice between ratifying that exemption and authorizing new seams outside this bug's scope is the maintainer's. +- Files: `QuickFiler/Controllers/EfcFormController.Actions.cs` (23.42% line, 33.33% branch; baseline 21.30% / 28.12%) and the residue of `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` remaining after B-1 (its `ConfigureBreadcrumbControl` wiring, 25 lines, caps the file at 79.3% line without a host seam). +- Problem: both files remain below the 80% line / 75% branch modified-file floor. Uncovered members: `ActionOkAsync` 0/28 lines, `CreateFolderAsync` 0/41, `ActionCancelAsync` 0/5, `RefreshSuggestionsAsync` 0/8 plus two lambdas, `ConfigureBreadcrumbControl` 0/22 plus three lambdas. None of these lines is changed by this branch, and this branch raised `EfcFormController.Actions.cs`. +- Decision required: (a) ratify the exemption for these members through `[ExcludeFromCodeCoverage]` or `coverage.config` as CLAUDE.md UT2 permits, or (b) authorize a follow-up issue for a message-box sink and a breadcrumb host factory seam with tests, or (c) accept the residue as recorded pre-existing debt for this item. +- Not for the atomic planner in this cycle: no plan task should add seams to these members without the decision. + +### B-4 Selected EFC descendant has no visual highlight + +- Severity: Blocking +Remediability: autonomous +Remediability-Evidence: the fix is one attribute-selector rule per theme block in a string constant already in the branch diff, plus a text-contract test; AC9's attribute-not-class decision is preserved. +- File: `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` (`LightThemeCss`, `DarkThemeCss`). +- Problem: `BreadcrumbHtmlRenderer.AppendDescendants` marks the selected descendant with `aria-selected="true"`, but no CSS rule targets that attribute, and `.rowwrap.selected .row` styles only the row's crumb line, not the `.children` container. After activating a grandchild, the row shows its own path highlighted while the filing destination is unmarked; manual check step 3 ("confirm it is highlighted") cannot pass. +- Expected behaviour after the fix: a selected descendant `.child` element renders with the same selection background as a selected row in each theme (`#cce8ff` light, `#264f78` dark). +- Fix: add `.child[aria-selected="true"] { background: #cce8ff; }` to `LightThemeCss` and `.child[aria-selected="true"] { background: #264f78; }` to `DarkThemeCss` (or an equivalent single rule using the theme colour), and a C# text-contract test asserting both rules, in `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` (297 lines) or a new Issue1002 test file. +- Verification commands: + - Toolchain in CLAUDE.md order. + - Grep of `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` for `aria-selected`: at least two hits. + - Update `evidence/other/manual-outlook-check..md` only if its steps change; the check remains pending human interaction. + +### B-3 Maintainer decision (recorded 2026-10-22T10:50Z) + +- Decision: option (a), ratify the CLAUDE.md UT2 COM/VSTO/WinForms coverage exemption for the uncovered MessageBox-bound action handlers and WebView2 host wiring in this item, applied through member-level `[ExcludeFromCodeCoverage]` attributes in source so the exemption is reviewable in the PR. +- Authority: the project maintainer selected "Ratify exemption" in the orchestrating session on 2026-10-22 in answer to the B-3 decision request. +- Reclassified: B-3 is now `Remediability: autonomous` for cycle 1. The "Do Not Do" prohibition on `ExcludeFromCodeCoverage` is lifted for exactly the members named below and no others. +- Members in scope: `EfcFormController.ActionOkAsync`, `EfcFormController.CreateFolderAsync`, `EfcFormController.ActionCancelAsync`, `EfcFormController.RefreshSuggestionsAsync` (only if it remains uncovered after the B-1 tests), and `EfcFormController.ConfigureBreadcrumbControl`. Each attribute carries a `Justification` naming the MessageBox or WebView2 host dependency and issue #1002. +- Not in scope: whole-file or `coverage.config` exclusions, and any member that a unit test can reach. +- Expected behaviour after the fix: per-file coverage of `QuickFiler/Controllers/EfcFormController.Actions.cs` and `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` is at least 80% line and 75% branch in the post-change Cobertura document. If compiler-generated async state machines or lambda closures of an attributed member still appear in the Cobertura document, record the residual figures and the generated class names in the coverage-comparison evidence rather than adding further exclusions. + +## Recommended In-Scope Items (non-blocking) + +- CR-1: `.child` became a flex container, so `text-overflow: ellipsis` no longer truncates long names; move `min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;` to `.child .name` in `BaseCss`. Same file as B-4. +- CR-4: one direct test of `ApplyDeleteGestureAsync` with a null viewer covers the remaining arm on line 102 of `EfcFormController.Actions.cs`. +- CR-2, CR-3, CR-5 (transient minus affordance during a fetch, nested `data-child-index`, Error-level logging of expected races): no change required in this cycle. + +## Non-Blocking Items + +- O-1: the PR-context summary classifies every changed C# file as docs/tooling; language scope was derived from `git diff`. +- O-2: skill layout conflicts (flat review-artifact names versus `audit//` folders; plan authorship) are recorded in the policy audit. +- O-3: the manual Outlook check stays pending human interaction by design (AC26). + +## Do Not Do + +- Do not change acceptance-criteria text, uncheck criteria, or add criteria. +- Do not add `ExcludeFromCodeCoverage` or `coverage.config` entries, or seams in `ActionOkAsync`, `CreateFolderAsync` or `ConfigureBreadcrumbControl`, without the B-3 decision. +- Do not edit files in the spec's excluded list (QFC router, selection session, coordinator, render projection, wiring, `BreadcrumbDescendantNode.cs`, `FolderBreadcrumb.html`, `Resources.resx`, `EfcDataModel.cs`). +- Do not extend the near-limit test files listed in the spec Constraints; keep every file at or under 500 lines. +- Do not add `ConfigureAwait(false)` or locks to `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs`. +- Do not edit policy documents under `.claude/rules/` or `.github/instructions/`. +- Do not commit raw `.trx`, `.coverage` or Cobertura documents; evidence stays as Markdown projections under `evidence/`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-inputs.2026-10-22T13-37.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-inputs.2026-10-22T13-37.md new file mode 100644 index 000000000..6234f1ced --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-inputs.2026-10-22T13-37.md @@ -0,0 +1,59 @@ +# Remediation Inputs: Issue #1002 (review 2026-10-22T13-37, re-audit after cycle 1) + +Review-Verdict: REMEDIATION_REQUIRED + +Remediation-Mode: design + +- Orchestrator mode note (2026-10-22T13:43Z): the reviewer proposed `proportionate`; the orchestrator set `design` because R-1 changes the text of a spec acceptance criterion (AC23), which the classification rule in `.claude/skills/orchestrate/SKILL.md` assigns to a design cycle. R-1 stays `autonomous`: the amendment records an exemption the maintainer has already ratified and introduces no new decision. +- Orchestrator scope note: N-1, N-3 and N-5 are in scope for cycle 2. They touch files already in the branch diff (`EfcFormController.Actions.cs`, `EfcFormControllerTests.Issue1002.cs`, `BreadcrumbHtmlRenderer.cs`, `BreadcrumbDocumentAssets.cs`) and are related to this item's changes. +- Feature folder: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002` +- Branch head reviewed: `b72ac1dbbfdb6b45d01a14dde9e91428a6bf41ac`; merge base `8e4ad1ecbf50aae00fe2c092c22ae457f4feb7a7` +- Source audit artifacts: + - `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/policy-audit.2026-10-22T13-37.md` + - `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/code-review.2026-10-22T13-37.md` + - `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/feature-audit.2026-10-22T13-37.md` +- Prior inputs: `remediation-inputs.2026-10-22T10-39.md`; its B-1, B-2, B-3 and B-4 are resolved (policy audit section 8). +- Next link: `atomic-planner` authors the cycle-2 remediation plan per `remediation-handoff-atomic-planner`; this review does not author the plan. +- Mode rationale: the single blocking finding is a text amendment to one acceptance criterion in `spec.md`; no source, test or evidence regeneration is required. + +## Remediation-Required Findings + +### R-1 AC23 text contradicts the ratified coverage exemption + +- Severity: Blocking +Remediability: autonomous +Remediability-Evidence: the maintainer already ratified the five member-level attributes in the recorded B-3 decision of remediation-inputs.2026-10-22T10-39.md; aligning AC23 with that recorded decision is a clerical spec amendment with no code change. If the orchestrator rules that acceptance-criterion wording is maintainer-owned, reclassify this finding as human_decision_required. +- File: `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md`, `## Acceptance Criteria`, AC23 (line 298). +- Problem: AC23 reads "... changed lines lose no coverage, no coverage exclusion is added, and the repository line and branch figures are recorded ..." and is checked `[x]`. The branch adds five `[ExcludeFromCodeCoverage]` attributes (`EfcFormController.ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `RefreshSuggestionsAsync`, `ConfigureBreadcrumbControl`). The B-3 decision lifted the plan's prohibition for these members, but the spec was not amended, so the criterion is false as written. +- Expected behaviour after the fix: AC23 states the ratified exception explicitly, `spec.md` carries a dated amendment note citing the decision, and every clause of AC23 is true of the head tree. +- Fix: + - Replace the clause "no coverage exclusion is added" in AC23 with "no coverage exclusion is added other than the five member-level `[ExcludeFromCodeCoverage]` attributes on `ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `RefreshSuggestionsAsync` and `ConfigureBreadcrumbControl` ratified by the maintainer under CLAUDE.md UT2 (remediation-inputs.2026-10-22T10-39.md, B-3 Maintainer decision); no `coverage.config` entry and no class-level exclusion is added". Change no other words of AC23 and no other criterion. + - Add a dated amendment entry to `spec.md` (for example under a `## Planner Amendments` heading) recording the AC23 change, its authority (maintainer decision 2026-10-22T10:50Z) and its reason (cycle-0 finding B-3). + - Keep AC23 checked; it is re-evaluated by the next review. +- Verification commands: + - `git diff -- docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md` shows only the AC23 clause change and the amendment entry. + - `git diff 8e4ad1ecb...HEAD -- '*.cs' | grep '^+' | grep -c ExcludeFromCodeCoverage` returns 5, matching the five members named in the amended text. + - `git diff --stat 8e4ad1ecb...HEAD -- coverage.config` is empty. + +## Recommended In-Scope Items (non-blocking) + +- N-1: reword the exemption comment above `ActionCancelAsync` in `QuickFiler/Controllers/EfcFormController.Actions.cs` to name the concrete `EfcViewer` dependency (`UiSyncContext` awaiter and `Close`); the method calls no `MessageBox`. If taken, rerun the full C# toolchain in CLAUDE.md order. +- N-3: in `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs`, give the three `delivered.Task` waits a failure exit that does not use wall-clock time (for example, complete the `TaskCompletionSource` from the fault-report path), so a fault before delivery fails the test instead of leaving it waiting. +- N-5: optionally wrap bare `.child` text in `` in the bridge `subfolderResult` handler and the renderer's key-less branch so the truncation rule applies; the first path is transient and the second has no production caller. + +## Non-Blocking Items + +- O-1: the PR-context summary classifies every changed C# file as docs/tooling; language scope was derived from `git diff`. +- O-2: `ExcludeFromCodeCoverage` has no `Justification` property on net48; adjacent comments are used (CS0246 probe evidence). +- O-3: the manual Outlook check stays pending human interaction by design (AC26). +- O-4: skill layout conflicts recorded in cycle 0 are unchanged. + +## Do Not Do + +- Do not change any acceptance criterion other than the AC23 clause named in R-1; do not uncheck or add criteria. +- Do not add, remove or move any `ExcludeFromCodeCoverage` attribute or `coverage.config` entry. +- Do not edit files in the spec's excluded list (QFC router, selection session, coordinator, render projection, wiring, `BreadcrumbDescendantNode.cs`, `FolderBreadcrumb.html`, `Resources.resx`, `EfcDataModel.cs`). +- Do not extend the near-limit test files listed in the spec Constraints; keep every file at or under 500 lines. +- Do not add `ConfigureAwait(false)` or locks to `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs`. +- Do not edit policy documents under `.claude/rules/` or `.github/instructions/`. +- Do not commit raw `.trx`, `.coverage` or Cobertura documents; evidence stays as Markdown projections under `evidence/`. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T10-45.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T10-45.md new file mode 100644 index 000000000..46a555f8f --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T10-45.md @@ -0,0 +1,109 @@ +# Remediation Plan: Issue #1002 (review 2026-10-22T10-39, cycle 1, R1, proportionate) + +- Feature folder (``): `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002` +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002` +- Inputs: `remediation-inputs.2026-10-22T10-39.md`, `policy-audit.2026-10-22T10-39.md`, `code-review.2026-10-22T10-39.md`, `feature-audit.2026-10-22T10-39.md`, `spec.md`. +- `SCRATCH` is `/issue-1002/`; every scratch script, staged copy and coverage projection lives under it and is recorded in evidence as the token `SCRATCH`. Tool routes and launchers are owned by the `execution-environment` skill. +- Plan edits are limited to the file set below. Acceptance criteria text of the original plan and spec is not changed. + +## Scope + +In scope: B-1 (EfcFormController.Breadcrumb.cs coverage), B-2 (BreadcrumbMessages.cs branch coverage), B-3 (maintainer decision recorded 2026-10-22T10:50Z in `remediation-inputs.2026-10-22T10-39.md`: ratify the CLAUDE.md UT2 COM/VSTO/WinForms exemption through member-level `[ExcludeFromCodeCoverage]`), B-4 (selected descendant highlight), CR-1 (ellipsis moved to `.child .name`), CR-4 (null-viewer test for `ApplyDeleteGestureAsync`). + +B-3 members, and no others: `ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync` (in `EfcFormController.Actions.cs`), `ConfigureBreadcrumbControl` (in `EfcFormController.Breadcrumb.cs`), and `RefreshSuggestionsAsync` (in `EfcFormController.Actions.cs`) only if it is still uncovered after the B-1 tests. Whole-file exclusions, `coverage.config` entries, seams, and any member a unit test can reach remain out of scope. + +Out of scope: CR-2, CR-3, CR-5; `coverage.config` changes and seams in the B-3 members; the spec's excluded file list (QFC router, selection session, coordinator, render projection, wiring, `BreadcrumbDescendantNode.cs`, `FolderBreadcrumb.html`, `Resources.resx`, `EfcDataModel.cs`); `ConfigureAwait(false)` or locks in `BreadcrumbBridgeRouter*.cs`; `.claude/rules/` and `.github/instructions/`; raw `.trx`, `.coverage` or Cobertura documents in git. + +File set: + +- Edit `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` (about 140 physical lines; already in the csproj as `Controllers\EfcFormControllerTests.Issue1002.cs`). Compiles as C# 7.3, so no `#nullable enable`. +- Create `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs` plus its `` in `UtilitiesCS.Test/UtilitiesCS.Test.csproj`. +- Edit `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` (about 297 lines). +- Edit `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` (constants `BaseCss`, `LightThemeCss`, `DarkThemeCss` only). +- Edit `QuickFiler/Controllers/EfcFormController.Actions.cs` and `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` for the B-3 member attributes (and a `using System.Diagnostics.CodeAnalysis;` directive where missing) and nothing else. + +Existing tests asserting changed behavior: the only test tree consumers of `BaseCss`, `LightThemeCss` and `DarkThemeCss` are in `BreadcrumbHtmlRendererIssue1002Tests.cs` (searched by symbol name across the repository). `BaseCss_Child_IsFlexRow` asserts that the single `.child {` rule contains `display: flex` and `align-items: center`; it stays unchanged because CR-1 leaves those declarations on `.child` and the `.child .name` rule does not match its `^\.child \{` pattern. The node suites under `QuickFiler.Test/Viewers/` assert the JavaScript asset, not these constants, and are unaffected. + +## Phases + +### Phase 0 — Baseline capture + +- [x] [P0-T1] Read the policies in the `policy-compliance-order` order (CLAUDE.md, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/csharp.md`) and the remediation inputs file, then write `/evidence/baseline/remediation-phase0-instructions-read..md` with `Timestamp:`, `Policy Order:` and the list of files read. Acceptance: the artifact exists with all three fields. +- [x] [P0-T2] Record physical line counts of `EfcFormControllerTests.Issue1002.cs`, `BreadcrumbHtmlRendererIssue1002Tests.cs` and `BreadcrumbDocumentAssets.cs` in `/evidence/remediation-baseline/file-sizes..md` (`Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`; count with `@(Get-Content -LiteralPath ).Count`). Acceptance: three counts recorded, each at or under 500. +- [x] [P0-T3] Run the coverage test route `scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` on the unmodified branch head (it applies the `\.claude\` exclusion and `/InIsolation` itself) and record the run in `/evidence/remediation-baseline/baseline-tests..md` with the pass and fail counts derived from the trx summary (no raw trx committed). Acceptance: artifact carries `EXIT_CODE: 0` and numeric pass counts. +- [x] [P0-T4] From the post-processed Cobertura of P0-T3, write the per-file union of class line nodes for `QuickFiler\Controllers\EfcFormController.Breadcrumb.cs` (expected 78/121 lines, 22/42 branches), `UtilitiesCS\OutlookObjects\Folder\BreadcrumbMessages.cs` (expected 7/14 branches) and the `condition-coverage` of each `?? throw new ArgumentNullException` line in `BreadcrumbMessages.cs`, to `/evidence/remediation-baseline/baseline-per-file-coverage..md`, using a helper stored under `SCRATCH`. Acceptance: the three figures are numeric; a figure differing from the expected one is recorded as measured and not adjusted. + +### Phase 1 — B-1, CR-4 and B-3: EfcFormController bind-path tests and ratified exemption + +All tests below are added to the `EfcFormControllerTests` partial in `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs`, reuse `CreateIssue1002Controller`, `SetPrivateField` and `GetPrivateField`, call the private members by reflection (no production change), use strict mocks, create no temporary files, and use no `Thread.Sleep` or `Task.Delay`. Where a fire-and-forget bind is observed, the test awaits delivery of the document through a `TaskCompletionSource` completed by the host mock's `NavigateToString` callback, or asserts directly when the strict-mock path completes synchronously. + +- [x] [P1-T1] Add `BindSourceFolderRows_WithViewerAndRouter_RetainsRowsAndBindsDocument`: invoking `BindSourceFolderRows` with one presented row leaves `_folderRows` equal to that array and delivers a document containing the row. Acceptance: test passes and covers the retain-and-bind path of `BindSourceFolderRows` and `BindFolderRows`. +- [x] [P1-T2] Add `BindSourceFolderRows_NullRows_RetainsEmptyArray`: invoking with null leaves `_folderRows` an empty array. Acceptance: test passes and exercises the `rows ?? Array.Empty` arm of `BindSourceFolderRows`. +- [x] [P1-T3] Add `BindSourceFolderRows_NullViewer_LeavesFolderRowsUnchanged`: with `_formViewer` set to null after controller creation, `_folderRows` keeps its prior value and no document is delivered. Acceptance: test passes and exercises the null-viewer arm. +- [x] [P1-T4] Add `BindSourceFolderRows_NullRouter_LeavesFolderRowsUnchanged`: with `_router` set to null, `_folderRows` keeps its prior value. Acceptance: test passes and exercises the null-router arm. +- [x] [P1-T5] Add `BindFolderRows_NullRows_BindsEmptyDocumentWithoutRetaining`: invoking `BindFolderRows` directly with null delivers a document and leaves `_folderRows` unchanged. Acceptance: test passes and exercises the `rows ?? Array.Empty` arm of `BindFolderRows`. +- [x] [P1-T6] Add `BindFolderRows_NullViewer_DoesNotBind` and `BindFolderRows_NullRouter_DoesNotBind` as two separate test methods: no document is delivered and no exception escapes. Acceptance: both pass and exercise both arms of the `BindFolderRows` guard. +- [x] [P1-T7] Add `ApplyDeleteGestureAsync_NullViewer_RetainsTrashRowAndDoesNotBind` (CR-4): with `_formViewer` null, the call completes without throwing, `_folderRows` contains the `EfcFormController.TrashRowText` entry exactly once, and no document is delivered. Acceptance: test passes and exercises the null-viewer arm of the guard in `ApplyDeleteGestureAsync`. +- [x] [P1-T8] Run `dotnet tool run csharpier format .` and then the scoped vstest run of `EfcFormControllerTests` through the repository's scoped route (with the `\.claude\` exclusion and `/InIsolation`), recording `/evidence/regression-testing/efc-bind-tests-pass..md` with the pass count of the new tests (9 expected) and no failures. Acceptance: all new tests and every pre-existing `EfcFormControllerTests` test pass; the formatter wrote no further change on a second run (`dotnet tool run csharpier check .` exits 0). +- [x] [P1-T9] Record the physical line count of `EfcFormControllerTests.Issue1002.cs` after formatting in `/evidence/qa-gates/file-sizes-remediation..md`. Acceptance: count at or under 500. +- [x] [P1-T10] B-3 sequencing decision, taken after the B-1 and CR-4 tests exist: run `scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` and record in `/evidence/qa-gates/b3-refresh-suggestions-decision..md` the per-method covered and valid line counts of `RefreshSuggestionsAsync` (including its async state machine and lambdas) from the post-processed Cobertura. Acceptance: the artifact states `ATTRIBUTE-REFRESHSUGGESTIONS: YES` when any of its lines is uncovered and `ATTRIBUTE-REFRESHSUGGESTIONS: NO` otherwise. +- [x] [P1-T11] Determine the attribute form for the net48 target (QuickFiler targets .NET Framework 4.8.1, where `ExcludeFromCodeCoverageAttribute` may not expose the `Justification` property that newer frameworks have): temporarily compile `ActionOkAsync` with `[ExcludeFromCodeCoverage(Justification = "...")]` through the analyzer build and record the outcome in `/evidence/other/exclude-attribute-form..md` as `ATTRIBUTE-FORM: JUSTIFICATION` (compiles) or `ATTRIBUTE-FORM: COMMENT` (compile error on the property). Acceptance: artifact states exactly one form with the compiler diagnostic id when the form is COMMENT; the following tasks apply the recorded form. +- [x] [P1-T12] In `EfcFormController.Actions.cs`, add the recorded attribute form to `ActionOkAsync`, `ActionCancelAsync` and `CreateFolderAsync`, each justification naming the `MessageBox` dependency and issue #1002 (form JUSTIFICATION: `Justification = "..."` argument; form COMMENT: parameterless attribute with an adjacent `//` comment carrying the same text), adding `using System.Diagnostics.CodeAnalysis;` if absent. Acceptance: `git grep -c -e ExcludeFromCodeCoverage -- QuickFiler/Controllers/EfcFormController.Actions.cs` reports 3 before P1-T14 and the project builds. +- [x] [P1-T13] In `EfcFormController.Breadcrumb.cs`, add the recorded attribute form to `ConfigureBreadcrumbControl` with a justification naming the WebView2 host dependency (`WebView2BreadcrumbHost`) and issue #1002, adding `using System.Diagnostics.CodeAnalysis;` if absent. Acceptance: `git grep -c -e ExcludeFromCodeCoverage -- QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` reports 1 and the project builds. +- [x] [P1-T14] When P1-T10 recorded `ATTRIBUTE-REFRESHSUGGESTIONS: YES`, add the recorded attribute form to `RefreshSuggestionsAsync` in `EfcFormController.Actions.cs` with a justification naming its `MessageBox`-bound dependency and issue #1002; when it recorded `NO`, make no edit. Acceptance: `git grep -c -e ExcludeFromCodeCoverage -- QuickFiler/Controllers/EfcFormController.Actions.cs` reports 4 for YES and 3 for NO, matching the recorded decision. + +### Phase 2 — B-2: BreadcrumbMessages null-guard tests + +New file `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessagesIssue1002Tests.cs` (namespace of the sibling `BreadcrumbMessageCodecIssue1002Tests`; `#nullable enable` is permitted in this project, as the sibling test uses it; null arguments are passed as `null!`). Each test asserts `ArgumentNullException` with `ParamName` through FluentAssertions. + +- [x] [P2-T1] Create the file with a class carrying `BreadcrumbInboundMessage_NullType_ThrowsWithParamName` (`type`) and `BreadcrumbInboundMessage_NullRowId_ThrowsWithParamName` (`rowId`), and add `` next to the existing Issue1002 entries in `UtilitiesCS.Test/UtilitiesCS.Test.csproj`. Acceptance: both tests pass; `git grep -n -e BreadcrumbMessagesIssue1002Tests -- UtilitiesCS.Test/UtilitiesCS.Test.csproj` returns exactly one hit. +- [x] [P2-T2] Add `BreadcrumbOutboundMessage_NullType_ThrowsWithParamName` using a private nested test subclass of `BreadcrumbOutboundMessage` that forwards a null type (the base constructor is protected and every production subclass passes a constant). Acceptance: test passes and the exception parameter name is `type`. +- [x] [P2-T3] Add `BreadcrumbRenderMessage_NullHtml_ThrowsWithParamName` (`html`). Acceptance: test passes. +- [x] [P2-T4] Add `BreadcrumbSubfolderResultMessage_NullRequestId_Throws`, `..._NullRowId_Throws` and `..._NullChildren_Throws` as three test methods asserting parameter names `requestId`, `rowId`, `children`. Acceptance: all three pass. +- [x] [P2-T5] Run `dotnet tool run csharpier format .` and the scoped vstest run of `BreadcrumbMessagesIssue1002Tests`, recording `/evidence/regression-testing/breadcrumb-messages-null-guards-pass..md` with the pass count (7 expected). Acceptance: 7 pass, 0 fail, and `dotnet tool run csharpier check .` exits 0. + +### Phase 3 — B-4 and CR-1: selected descendant highlight and ellipsis + +Tests are added to `BreadcrumbHtmlRendererIssue1002Tests.cs` before the production change so the change is proven by a failing-then-passing run. The literals asserted are the new rules: `.child[aria-selected="true"] { background: #cce8ff; }` in `LightThemeCss`, `.child[aria-selected="true"] { background: #264f78; }` in `DarkThemeCss`, and a `.child .name {` rule in `BaseCss` carrying `min-width: 0`, `overflow: hidden`, `text-overflow: ellipsis` and `white-space: nowrap`. + +- [x] [P3-T1] Add `LightThemeCss_SelectedChild_UsesSelectionBackground` asserting the light rule above. Acceptance: test exists and fails before P3-T5. +- [x] [P3-T2] Add `DarkThemeCss_SelectedChild_UsesSelectionBackground` asserting the dark rule above. Acceptance: test exists and fails before P3-T5. +- [x] [P3-T3] Add `BaseCss_ChildName_CarriesEllipsisRule`: the `.child .name {` rule contains the four declarations and the single `.child {` rule no longer contains `text-overflow`, while it still contains `display: flex` and `align-items: center`. Acceptance: test exists and fails before P3-T6. +- [x] [P3-T4] [expect-fail] Run the scoped vstest of `BreadcrumbHtmlRendererIssue1002Tests` and record `/evidence/regression-testing/css-highlight-fail-before..md` (Timestamp, Command, EXIT_CODE, Output Summary) showing the three new tests failing and every pre-existing test in the class passing. Acceptance: exactly the three new tests fail. +- [x] [P3-T5] In `BreadcrumbDocumentAssets.cs`, append the light rule to `LightThemeCss` and the dark rule to `DarkThemeCss` (attribute selector only; no class is introduced, preserving AC9). Acceptance: `git grep -c -e aria-selected -- UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` reports at least 2; the two theme tests pass. +- [x] [P3-T6] In `BaseCss`, remove `text-overflow`, `overflow` and `white-space` from the `.child` rule (keeping `display: flex; align-items: center;`) and add the `.child .name` rule with the four declarations (CR-1). Acceptance: `BaseCss_ChildName_CarriesEllipsisRule` and the unchanged `BaseCss_Child_IsFlexRow` pass. +- [x] [P3-T7] Search for `.child` emitters that do not wrap the name in `.name` (the first-level leaf-children branch of `BreadcrumbHtmlRenderer` and the `makeSubfolders` path in the bridge JavaScript) and record in `/evidence/other/child-name-markup-check..md` which emitters carry a `.name` span and which render bare text. No excluded file is edited; an emitter outside the file set is recorded as an observation. Acceptance: artifact lists each emitter with its markup shape, and `manual-outlook-check` step 3 is recorded as unchanged (the existing manual-check artifact is not edited). +- [x] [P3-T8] Run the scoped vstest of `BreadcrumbHtmlRendererIssue1002Tests` and record `/evidence/regression-testing/css-highlight-pass-after..md`. Acceptance: all tests in the class pass (pre-existing count plus 3), 0 fail. + +### Phase 4 — Full QA loop and per-finding verification + +Loop rule: run P4-T1 to P4-T5 in order; if any task fails or changes a tracked file, fix the cause and restart from P4-T1. Record each pass's artifacts with a new stamp. + +- [x] [P4-T1] Apply formatting with `dotnet tool run csharpier format .`; record `/evidence/qa-gates/remediation-format..md` with the observation that `git status --porcelain` lists no tracked change attributable to the formatter beyond the files of this plan. Acceptance: exit 0 and the porcelain observation recorded. +- [x] [P4-T2] Verify read-only with `dotnet tool run csharpier check .`; record `remediation-format-check..md`. Acceptance: exit 0 and the success-case summary line the tool printed is quoted in `Output Summary:`. +- [x] [P4-T3] Run the analyzer build (`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`); record `remediation-analyzers..md`. Acceptance: exit 0 with the warning and error counts from the msbuild summary. +- [x] [P4-T4] Run the type-check build (`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`; no `/p:Nullable=enable`); record `remediation-typecheck..md`. Acceptance: exit 0 with 0 errors. +- [x] [P4-T5] Run `scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .`; record `remediation-tests..md` with trx-derived pass and fail counts and the one-line first-party coverage summary (no raw trx or Cobertura committed). Acceptance: exit 0, 0 failed, pass count at least the P0-T3 count plus the 19 new tests (9 + 7 + 3). +- [x] [P4-T6] After the final clean pass, refresh `artifacts/csharp/coverage.xml` from `coverage/coverage.cobertura.xml` by the same derivation recorded in `/evidence/baseline/baseline-coverage.2026-10-22T09-54.md`, and record the refresh in `/evidence/qa-gates/coverage-xml-refresh..md` (file modification time later than the P4-T5 run; `artifacts/` is not staged). Acceptance: artifact states the source and destination and the first-party line and branch totals. +- [x] [P4-T7] B-1 verification: with the SCRATCH helper from P0-T4, compute the per-file union of class line nodes for `QuickFiler\Controllers\EfcFormController.Breadcrumb.cs` from the post-processed Cobertura and record it in `/evidence/qa-gates/b1-per-file-coverage..md`. Acceptance: covered lines at least 84 and covered branches at least 24 (covered counts, because the B-3 attribute on `ConfigureBreadcrumbControl` removes lines from the valid-line denominator). +- [x] [P4-T8] B-2 verification: compute `condition-coverage` for each line of `BreadcrumbMessages.cs` containing `throw new ArgumentNullException` (seven lines) and the file branch total, and record in `/evidence/qa-gates/b2-per-file-coverage..md`. Acceptance: each of the seven lines at 100% (2/2) and file branch coverage at least 11 of 14. +- [x] [P4-T9] B-3 verification after the final clean pass: compute per-file line and branch coverage from the post-processed Cobertura for `QuickFiler\Controllers\EfcFormController.Actions.cs` and `QuickFiler\Controllers\EfcFormController.Breadcrumb.cs`, and list every class node of those files that remains (compiler-generated async state machines and lambda closures of the attributed members) with its covered and valid counts and class name, in `/evidence/qa-gates/b3-per-file-coverage..md`. Acceptance: each file is at least 80% line and at least 75% branch; residual generated classes are recorded and no further exclusion is added to reach the figure (a shortfall is reported as a measured failure and the plan outcome is remediation-required). +- [x] [P4-T10] B-4 verification: run `git grep -c -e aria-selected -- UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` and record the count in `/evidence/qa-gates/b4-grep..md`. Acceptance: count is at least 2, with the success-case count quoted. +- [x] [P4-T11] Record the physical line counts of every edited or created `.cs` file (the three test files, `BreadcrumbDocumentAssets.cs`, `EfcFormController.Actions.cs`, `EfcFormController.Breadcrumb.cs`) in `/evidence/qa-gates/file-sizes-remediation..md` after the final format pass. Acceptance: every count at or under 500. +- [x] [P4-T12] Write `/evidence/qa-gates/coverage-comparison..md` (new stamp; the earlier `coverage-comparison.2026-10-22T10-26.md` is not edited) comparing baseline (P0-T4), post-remediation per-file figures for `EfcFormController.Breadcrumb.cs` (B-1), `BreadcrumbMessages.cs` (B-2) and `EfcFormController.Actions.cs` (B-3, CR-4), the residual generated-class figures from P4-T9, and repository first-party line and branch totals against the 80% and 75% floors. Acceptance: states numeric baseline, final and delta for each; the repository totals are not lowered; the B-3 exemption is recorded as member-level attributes only, with no `coverage.config` entry. +- [x] [P4-T13] Scope-fence evidence, recorded in `/evidence/qa-gates/scope-fences-remediation..md` (the executor does not stage or commit; the orchestrator owns `git add` and `git commit`): the output of `git diff --name-only` anchored on the merge base of `origin/main` and `HEAD`, the `git status --porcelain` listing (which also shows untracked files the diff cannot), the added `ExcludeFromCodeCoverage` lines from `git diff` anchored on that merge base over `*.cs`, and `git diff --stat` of `coverage.config` against that merge base. Acceptance: no changed or untracked file is on the spec's excluded list or under `.claude/rules/`; the added attribute lines sit on exactly `ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `ConfigureBreadcrumbControl` and, only when P1-T10 recorded YES, `RefreshSuggestionsAsync`; no `coverage.config` change; no `.trx`, `.coverage` or Cobertura document listed. +- [x] [P4-T14] Walk the policy-audit, code-review and feature-audit templates under `docs/features/templates/policy_audit/` against the branch diff from the merge base and write `/evidence/qa-gates/self-review..md` (new stamp; earlier self-review kept) with one PASS, FAIL or N/A row per checklist item and the status of B-1, B-2, B-3 (exemption ratified by the maintainer, member-level attributes), B-4, CR-1 and CR-4. Acceptance: no FAIL row; a FAIL is fixed and the loop restarts from P4-T1 before this file is rewritten. + +## Size Justification + +The plan has 45 tasks because it carries six remediation items (B-1, B-2, B-3, B-4, CR-1, CR-4) across three test files, three production files and four independent per-file verification gates; each test is one task to keep a single binary outcome. Splitting the item would separate the B-3 attribute work from the B-1 tests that decide which members stay uncovered. + +## Pre-Mortem + +- Review checklists: P4-T14 walks the three audit templates and records a row per item; the six in-scope findings (B-1, B-2, B-3, B-4, CR-1, CR-4) are each mapped to implementing and verification tasks. +- Coverage: B-1 and B-2 are proved by per-file Cobertura unions and per-line `condition-coverage` (P4-T7, P4-T8), not by the repository total; B-1 asserts covered counts because the B-3 attribute shrinks the denominator. B-3 is applied only to the five named members, after the B-1 tests (P1-T10 decides `RefreshSuggestionsAsync` from a measured run), in the attribute form P1-T11 proves compiles on net48 (Justification property, else parameterless attribute with an adjacent comment). P4-T9 verifies both files at 80% line and 75% branch and records residual compiler-generated classes without adding exclusions; if the figure is short it is reported as a failure rather than closed by more exclusions. P0-T4 captures baseline figures as measured; P4-T12 records baseline, final and delta. No whole-file, `coverage.config` or seam change is made. +- Mirror parity: not applicable. No CI job, required check, tool route or batch-budget fact in the execution-environment brief changes, and no mirrored `.claude`, `.agents` or bundle file is edited. +- File size: P0-T2 and P4-T11 measure physical lines of every edited or created `.cs` file after formatting; the largest edited file is about 300 lines before additions and the additions are under 130 lines. The new test file is created rather than extending the 500-line-adjacent files named in the spec Constraints. +- Test isolation: new tests build their own strict-mock controller and router per test, hold no static state, create no temporary files and use no sleeps or delays. The full test route (P4-T5) runs with the repository default parallel settings and is not worked around with `Workers=1`, `[DoNotParallelize]` or retries; fire-and-forget binds are awaited through a `TaskCompletionSource`. +- CI-only behavior: the CSS tests are text-contract tests over constants and read no gitignored `artifacts/` state; the QuickFiler.Test file stays C# 7.3 compatible (no `#nullable enable`, no nullable annotations); the new UtilitiesCS.Test file carries its `` because the legacy csproj lists compile items explicitly, which P2-T1 verifies with a `git grep` over the csproj; `git grep` is used only on tracked files. +- Evidence: every artifact is a Markdown projection under `/evidence//` with `Timestamp:`, `Command:`, `EXIT_CODE:` and `Output Summary:`; consumers select the greatest `.*.md`; P3-T4 is the expect-fail proof for the CSS fix; no raw trx, `.coverage` or Cobertura document and no host path or scratchpad path is committed (`SCRATCH` is recorded as a token); P4-T13 checks the changed and untracked listing for forbidden documents; the executor does not stage or commit, and the porcelain listing is evidence rather than a clean-tree assertion because the orchestrator owns the commit. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T13-43.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T13-43.md new file mode 100644 index 000000000..34bad9c01 --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/remediation-plan.2026-10-22T13-43.md @@ -0,0 +1,101 @@ +# Remediation Plan: Issue #1002 (review 2026-10-22T13-37, cycle 2, R1, design) + +- Feature folder (``): `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002` +- Branch: `bug/efc-qfc-folder-tree-selection-and-nesting-1002` +- Inputs: `remediation-inputs.2026-10-22T13-37.md` (Remediation-Mode: design), `policy-audit.2026-10-22T13-37.md`, `code-review.2026-10-22T13-37.md`, `feature-audit.2026-10-22T13-37.md`, `spec.md`, and the B-3 Maintainer decision section of `remediation-inputs.2026-10-22T10-39.md`. +- `SCRATCH` is `/issue-1002/`; every scratch script, staged copy and coverage projection lives under it and is recorded in evidence as the token `SCRATCH`. Tool routes and launchers are owned by the `execution-environment` skill. +- Merge base: every anchored `git diff` in this plan uses the merge-base derivation of `origin/main` and `HEAD` (`git merge-base origin/main HEAD`) as its ref. + +## Scope + +In scope: R-1 (blocking, autonomous: amend only the named clause of AC23 in `spec.md`, keep AC23 checked, add a dated amendment entry citing the maintainer decision of 2026-10-22T10:50Z), N-1 (exemption comment above `ActionCancelAsync`), N-3 (non-wall-clock failure exit for the three `delivered.Task` waits), N-5, narrowed by the coordinator ruling below (wrap bare `.child` text in `` in the bridge `subfolderResult` handler only). + +Ruling on the AC9 tension (coordinator, recorded in this plan and in the P5-T11 self-review): AC9 is authoritative and stays true. AC9 states that key-less children "render exactly as before". The key-less branch of `BreadcrumbHtmlRenderer` has no production caller (13-37 review), so wrapping its text adds no user-visible value while it would falsify a checked criterion; that branch change is therefore dropped from N-5. The retained change is in the `subfolderResult` handler of `BreadcrumbDocumentAssets.BridgeJs`, which is the client-side transient render of the bridge page script, not the AC9 server-side key-less render of `BreadcrumbHtmlRenderer`; AC9 does not govern it. `BreadcrumbHtmlRenderer.cs` is not touched this cycle. + +Out of scope (inputs "Do Not Do"): any change to an acceptance criterion other than the AC23 clause; adding, removing or moving any `ExcludeFromCodeCoverage` attribute or any `coverage.config` entry; the spec's excluded file list (QFC router, selection session, coordinator, render projection, wiring, `BreadcrumbDescendantNode.cs`, `FolderBreadcrumb.html`, `Resources.resx`, `EfcDataModel.cs`); the near-limit test files named in the spec Constraints; `ConfigureAwait(false)` or locks in `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs`; `.claude/rules/` and `.github/instructions/`; raw `.trx`, `.coverage` or Cobertura documents in git. + +File set: + +- Edit `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md` (AC23 clause plus a new `## Planner Amendments` section at the end). +- Edit `QuickFiler/Controllers/EfcFormController.Actions.cs` (comment text above `ActionCancelAsync` only; the attribute and the method body are untouched). +- Edit `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` (313 physical lines before the change; the file compiles as C# 7.3, so no `#nullable enable` and no nullable annotations). +- Edit `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` (the `subfolderResult` handler inside `BridgeJs`). `BreadcrumbHtmlRenderer.cs` is not edited. +- Edit `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` (343 physical lines before the change). +- No new source or test file, so no csproj edit. + +Existing tests asserting changed behavior (N-5; found by searching `UtilitiesCS.Test`, `QuickFiler.Test` and the other test trees for `data-child-index`, `class="child"`, `renderedChildActivate`, `textContent`, `subfolderResult`, `displayName` and the `BridgeJs` symbol). Dispositions: + +- `BreadcrumbHtmlRendererIssue1002Tests.RenderRowFragment_KeylessChildren_RenderUnchanged` asserts the exact key-less child tail `title="...">Q3
`; unchanged, because the renderer is not edited and the server-side key-less render stays exactly as before (AC9). +- `BreadcrumbHtmlRendererTests.Issue439ActiveAncestorChildrenAndEmbeddedBridgeUseTypedStoppedActivation` pins `class="child" data-child-index="0"` and the `renderedChildActivate` JavaScript; unchanged because the renderer prefix and the `renderedChildActivate` branch are not touched (only the `subfolderResult` handler changes). +- `BreadcrumbHtmlRendererTests.RenderRowFragment_ExpandedLeaf_RendersEncodedChildren` asserts the rendered key-less markup; unchanged because the renderer is not edited. +- The keyed-child, `BridgeJs` ordering, `BaseCss` and theme tests in `BreadcrumbHtmlRendererIssue1002Tests.cs` index on `.child[data-node-id]`, `.child[data-child-index]`, `type: 'renderedChildActivate'` and CSS rules, none of which change; unchanged. +- `QuickFiler.Test` router and codec tests assert the `subfolderResult` JSON message (display name and path fields), not the page script; unchanged. The node suites under `QuickFiler.Test/Viewers/` assert `FolderBreadcrumb.js`, not `BridgeJs`; unchanged. +- Every other renderer, `BridgeJs` ordering, `BaseCss` and theme test searched above asserts markup or script that the `subfolderResult` edit does not touch; the only test that reads the `subfolderResult` handler text is the new one added in P4-T1. AC9 and AC22 are not affected. + +## Phases + +### Phase 0 — Baseline capture + +- [x] [P0-T1] Read the policies in the `policy-compliance-order` order (CLAUDE.md, `.claude/rules/general-code-change.md`, `.claude/rules/general-unit-test.md`, `.claude/rules/csharp.md`), the inputs file, and the three 13-37 audit artifacts, then write `/evidence/baseline/cycle2-phase0-instructions-read..md` with `Timestamp:`, `Policy Order:` and the list of files read. Acceptance: the artifact exists with all three fields. +- [x] [P0-T2] Record the physical line counts of `EfcFormControllerTests.Issue1002.cs`, `BreadcrumbHtmlRendererIssue1002Tests.cs`, `BreadcrumbDocumentAssets.cs` and `EfcFormController.Actions.cs` in `/evidence/remediation-baseline/cycle2-file-sizes..md` (`Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`; count with `@(Get-Content -LiteralPath ).Count`). Acceptance: four counts recorded, each at or under 500. +- [x] [P0-T3] Run `scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .` on the unmodified branch head and record `/evidence/remediation-baseline/cycle2-baseline-tests..md` with trx-derived pass and fail counts and the one-line first-party coverage summary (no raw trx or Cobertura committed). Acceptance: artifact carries `EXIT_CODE: 0` and numeric pass counts. +- [x] [P0-T4] With a helper stored at `SCRATCH/issue-1002-r2-perfile.ps1`, compute from the post-processed Cobertura of P0-T3 the per-file union of class line and branch nodes for `QuickFiler\Controllers\EfcFormController.Actions.cs` and `UtilitiesCS\OutlookObjects\Folder\BreadcrumbDocumentAssets.cs`, plus the first-party repository line and branch totals, and write them to `/evidence/remediation-baseline/cycle2-baseline-per-file-coverage..md`. Acceptance: eight per-file figures (covered and valid, line and branch, for two files) and two repository figures are numeric; a figure is recorded as measured and not adjusted. +- [x] [P0-T5] Record the R-1 reference counts at the unmodified head in `/evidence/remediation-baseline/cycle2-exclude-attribute-baseline..md`: the number of `ExcludeFromCodeCoverage` lines added in `*.cs` files by `git diff` anchored on the merge base, and `git diff --stat` of `coverage.config` against the merge base. Acceptance: the attribute count is 5 and the `coverage.config` stat is empty; any other value is recorded as measured and stops the plan as BLOCKED. + +### Phase 1 — R-1: AC23 amendment + +- [x] [P1-T1] In `/spec.md`, `## Acceptance Criteria`, AC23, replace only the clause "no coverage exclusion is added" with the text "no coverage exclusion is added other than the five member-level `[ExcludeFromCodeCoverage]` attributes on `ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `RefreshSuggestionsAsync` and `ConfigureBreadcrumbControl` ratified by the maintainer under CLAUDE.md UT2 (remediation-inputs.2026-10-22T10-39.md, B-3 Maintainer decision); no `coverage.config` entry and no class-level exclusion is added". Change no other word of AC23, keep its `[x]` mark, and edit no other criterion. Acceptance: the AC23 line still starts with the checked marker and contains the quoted replacement text; the AC22 and AC24 lines are byte-identical to their pre-edit text. +- [x] [P1-T2] Append a `## Planner Amendments` section at the end of `/spec.md` with one dated entry: date 2026-10-22, the AC23 change (clause replaced, criterion remains checked), authority (maintainer decision recorded 2026-10-22T10:50Z in `remediation-inputs.2026-10-22T10-39.md`, B-3 Maintainer decision), and reason (cycle-0 finding B-3; the five attributes exist on the branch and the criterion was false as written). Acceptance: the heading and one entry naming the date, authority and reason exist, and no other heading of `spec.md` changed. +- [x] [P1-T3] Verify R-1 and record `/evidence/qa-gates/cycle2-r1-verification..md`: `git diff HEAD --stat -- /spec.md` and the full `git diff HEAD -- /spec.md` listing show exactly two hunks (the AC23 clause and the amendment section); the added `ExcludeFromCodeCoverage` lines in `*.cs` files counted from `git diff` anchored on the merge base equal 5 and name the five amended members; `git diff --stat` of `coverage.config` against the merge base is empty; `git status --porcelain` is quoted for the untracked listing. Acceptance: two hunks, count 5, empty `coverage.config` stat. + +### Phase 2 — N-1: exemption comment above ActionCancelAsync + +- [x] [P2-T1] In `QuickFiler/Controllers/EfcFormController.Actions.cs`, reword the two-line exemption comment immediately above `[ExcludeFromCodeCoverage]` on `ActionCancelAsync` so it names the concrete `EfcViewer` dependency (the `UiSyncContext` awaiter and `Close` on the live WinForms viewer) and no longer names `MessageBox`. Keep the leading "Coverage exemption (#1002, maintainer-ratified CLAUDE.md UT2 COM/VSTO/WinForms exemption)" sentence form of the sibling comments, do not write the attribute token inside the new comment, and leave the attribute, the method signature and the method body untouched. Acceptance: the comment above `ActionCancelAsync` contains `UiSyncContext` and `Close` and not `MessageBox`; the `MessageBox` occurrence count of the file drops by exactly one against the P0 state; the method body is unchanged. +- [x] [P2-T2] Record `/evidence/qa-gates/cycle2-n1-verification..md` with the before and after `MessageBox` occurrence counts of `EfcFormController.Actions.cs` (a search with `-c`), the reworded comment text, and the added `ExcludeFromCodeCoverage` line count from `git diff` anchored on the merge base (must still be 5). Acceptance: counts recorded and the attribute count is 5. + +### Phase 3 — N-3: failure exit for the three delivered waits + +The three waits are in `BindSourceFolderRows_WithViewerAndRouter_RetainsRowsAndBindsDocument`, `BindSourceFolderRows_NullRows_RetainsEmptyArray` and `BindFolderRows_NullRows_BindsEmptyDocumentWithoutRetaining`. Design: `CreateIssue1002Controller` wires `BoundaryErrorSink` so a fault reported at the bind boundary calls `TrySetException` on the supplied `TaskCompletionSource`; the waits then fault instead of hanging. With `withGlobals: false` the bind boundary faults before its first await (the `_globals.Ol` argument of the `BindRowsAsync` call is evaluated, and throws, before the call is made; the preceding score projection has no await), so the fault is reported synchronously and a proof test can observe it with no wait, sleep, `Task.Delay` or wall-clock time. `ActionDeleteAsync_BindFault_IsReportedAtBindBoundary` installs its own sink after creation and passes no `delivered`, so it is unaffected. + +- [x] [P3-T1] Add `BindFolderRows_FaultBeforeDelivery_FaultsDeliveredTaskPromptly` to `EfcFormControllerTests.Issue1002.cs`: build the controller with `withGlobals: false` and a `delivered` source, invoke `BindFolderRows` through `InvokeIssue1002Bind`, then assert `delivered.Task.IsFaulted` is true and `delivered.Task.Exception` is non-null, with a failure message stating that a pre-delivery fault must fail the wait. The test performs no await, no sleep and no delay. Acceptance: the test exists, compiles as C# 7.3, and (before P3-T3) fails. +- [x] [P3-T2] [expect-fail] Run the scoped vstest of `EfcFormControllerTests` through the repository's scoped route and record `/evidence/regression-testing/cycle2-delivered-wait-fail-before..md` (`Timestamp:`, `Command:`, `EXIT_CODE:`, `Output Summary:`). Acceptance: exactly the one new test fails (assertion failure, not a hang) and every pre-existing `EfcFormControllerTests` test passes. +- [x] [P3-T3] In `CreateIssue1002Controller`, after the controller is created, set `controller.BoundaryErrorSink` to a delegate that calls `delivered?.TrySetException(exception)`; leave the signature, the strict mocks and the `NavigateToString` callback unchanged. Acceptance: the project builds and the three wait tests still pass unchanged (they keep `await delivered.Task`). +- [x] [P3-T4] Run `dotnet tool run csharpier format .` (success-case summary line: the formatter prints `Formatted 0 files` style output when nothing changed; the observation recorded is the `git status --porcelain` listing before and after) and the scoped vstest of `EfcFormControllerTests`; record `/evidence/regression-testing/cycle2-delivered-wait-pass-after..md` with pass and fail counts. Acceptance: the new test and every pre-existing test of the class pass, 0 fail. +- [x] [P3-T5] Record the physical line count of `EfcFormControllerTests.Issue1002.cs` after formatting in `/evidence/qa-gates/cycle2-file-sizes-n3..md`. Acceptance: count at or under 500. + +### Phase 4 — N-5: wrap bare child text in the name span + +Production shape asserted: the bridge `subfolderResult` handler creates the `child` div, appends a `span` whose class is `name` and whose `textContent` is the display name, and sets the div title to the full path. The server-side key-less render of `BreadcrumbHtmlRenderer` is deliberately unchanged (AC9 ruling in Scope). + +- [x] [P4-T1] Add `BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan` to `BreadcrumbHtmlRendererIssue1002Tests.cs`: the substring of `BreadcrumbDocumentAssets.BridgeJs` from the `msg.type === 'subfolderResult'` branch to the end contains `name.className = 'name';` and `name.textContent = c.displayName;` and `div.appendChild(name);`, and does not contain `div.textContent`. Acceptance: test exists, uses `System.StringComparison.Ordinal` for its index, and (before P4-T3) fails. +- [x] [P4-T2] [expect-fail] Run the scoped vstest of `BreadcrumbHtmlRendererIssue1002Tests` and record `/evidence/regression-testing/cycle2-child-name-fail-before..md`. Acceptance: exactly the one test of P4-T1 fails (assertion failure) and every other test of the class, including `RenderRowFragment_KeylessChildren_RenderUnchanged`, passes. +- [x] [P4-T3] In `BreadcrumbDocumentAssets.cs`, in the `subfolderResult` handler of `BridgeJs`, replace the `div.textContent = c.displayName;` statement with creation of a `span` named `name` (class `name`, `textContent` set to the display name) appended to the div, keeping `div.className = 'child'` and `div.title = c.fullPath;` and the string-concatenation form of the constant. Acceptance: `BridgeJs_SubfolderResultHandler_WrapsChildNameInNameSpan` passes. +- [x] [P4-T4] Run `dotnet tool run csharpier format .` (observation: `git status --porcelain` before and after) and the scoped vstest of `BreadcrumbHtmlRendererIssue1002Tests` and `BreadcrumbHtmlRendererTests`; record `/evidence/regression-testing/cycle2-child-name-pass-after..md`. Acceptance: all tests of both classes pass (pre-existing count plus 1 new), 0 fail, including `RenderRowFragment_KeylessChildren_RenderUnchanged` with its assertions unchanged. +- [x] [P4-T5] Write `/evidence/other/cycle2-child-name-dispositions..md` listing, for every test found by the Scope search, its disposition (unchanged because unaffected) and the search commands used with their hit lists, and recording that `BreadcrumbHtmlRenderer.cs` is unedited (an empty path-limited diff against the merge base of origin/main and HEAD). Acceptance: every test named in the Scope dispositions appears with a disposition, and the renderer path diff is empty. +- [x] [P4-T6] Record the physical line counts of `BreadcrumbHtmlRendererIssue1002Tests.cs` and `BreadcrumbDocumentAssets.cs` after formatting in `/evidence/qa-gates/cycle2-file-sizes-n5..md`. Acceptance: every count at or under 500. + +### Phase 5 — Full QA loop and verification + +Loop rule: run P5-T1 to P5-T5 in order; if any task fails or changes a tracked file, fix the cause and restart from P5-T1, recording each pass with a new stamp. + +- [x] [P5-T1] Apply formatting with `dotnet tool run csharpier format .`; record `/evidence/qa-gates/cycle2-format..md` with the `git status --porcelain` observation that the formatter changed no tracked file beyond this plan's file set. Acceptance: exit 0 and the observation recorded. +- [x] [P5-T2] Verify read-only with `dotnet tool run csharpier check .`; record `cycle2-format-check..md`. Acceptance: exit 0 and the success-case summary line the tool printed is quoted in `Output Summary:`. +- [x] [P5-T3] Run the analyzer build (`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`); record `cycle2-analyzers..md`. Acceptance: exit 0 with the warning and error counts from the msbuild summary. +- [x] [P5-T4] Run the type-check build (`msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true`; no `/p:Nullable=enable`); record `cycle2-typecheck..md`. Acceptance: exit 0 with 0 errors. +- [x] [P5-T5] Run `scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot .`; record `cycle2-tests..md` with trx-derived pass and fail counts and the one-line first-party coverage summary (no raw trx or Cobertura committed). Acceptance: exit 0, 0 failed, pass count at least the P0-T3 count plus 2 (the new tests are those of P3-T1 and P4-T1). +- [x] [P5-T6] After the final clean pass, refresh `artifacts/csharp/coverage.xml` from `coverage/coverage.cobertura.xml` by the same derivation recorded in `/evidence/qa-gates/coverage-xml-refresh.2026-10-22T13-32.md`, and record `/evidence/qa-gates/cycle2-coverage-xml-refresh..md` (destination modification time later than the P5-T5 run; `artifacts/` is not staged). Acceptance: artifact states source, destination and the first-party line and branch totals. +- [x] [P5-T7] With the helper from P0-T4 compute the post-change per-file figures for the two files and the repository totals, and write `/evidence/qa-gates/cycle2-coverage-comparison..md` (new stamp; earlier comparisons are not edited) with baseline, final and delta for each figure. Acceptance: repository line and branch totals are not lowered against P0-T4; each of the two touched production files (`EfcFormController.Actions.cs`, `BreadcrumbDocumentAssets.cs`) is at least 80% line and at least 75% branch (a shortfall is reported as a measured failure and the plan outcome is remediation-required; no exclusion is added to close it). +- [x] [P5-T8] Record the physical line counts of every edited `.cs` file (both test files, `BreadcrumbDocumentAssets.cs`, `EfcFormController.Actions.cs`) after the final format pass in `/evidence/qa-gates/cycle2-file-sizes..md`. Acceptance: every count at or under 500. +- [x] [P5-T9] Scope-fence evidence in `/evidence/qa-gates/cycle2-scope-fences..md` (the executor does not stage or commit): `git diff --name-only` anchored on the merge base, the `git status --porcelain` listing, the added `ExcludeFromCodeCoverage` lines from `git diff` anchored on the merge base over `*.cs`, `git diff --stat` of `coverage.config`, and a search of `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs` for `ConfigureAwait(false)`. Acceptance: no changed or untracked file is on the spec's excluded list or under `.claude/rules/` or `.github/instructions/`; the added attribute count is 5; `coverage.config` is unchanged; no `.trx`, `.coverage` or Cobertura document is listed; the router search returns no hit. +- [x] [P5-T10] Re-run the R-1 verification of P1-T3 against the final tree and record `/evidence/qa-gates/cycle2-r1-final..md`. Acceptance: the spec diff against `HEAD` still shows only the AC23 clause change and the amendment section, the attribute count is 5, and the `coverage.config` stat is empty. +- [x] [P5-T11] Walk the policy-audit, code-review and feature-audit templates under `docs/features/templates/policy_audit/` against the branch diff from the merge base and write `/evidence/qa-gates/self-review..md` (new stamp; earlier self-reviews kept) with one PASS, FAIL or N/A row per checklist item, the status of R-1, N-1, N-3 and N-5, and a row recording the coordinator ruling: AC9 stays authoritative and true, the key-less branch of `BreadcrumbHtmlRenderer` was deliberately left unedited because it has no production caller, and the retained `subfolderResult` name-span wrap is the client-side transient render that AC9 does not govern. Acceptance: no FAIL row; a FAIL is fixed and the loop restarts from P5-T1 before this file is rewritten. This is the final task; the orchestrator owns `git add` and `git commit`. + +## Pre-Mortem + +- Review checklists: P5-T11 walks the three audit templates and records a row per item; R-1, N-1, N-3 and N-5 each map to implementing tasks (P1, P2, P3, P4) and verification tasks (P1-T3, P2-T2, P3-T4, P4-T4), and the AC9 ruling is recorded in Scope and in the P5-T11 self-review for the next review. +- Coverage: the touched production files are `EfcFormController.Actions.cs` (comment only) and `BreadcrumbDocumentAssets.cs` (string constant); P0-T4 captures their baseline per-file figures and P5-T7 asserts 80% line and 75% branch per file plus no lowering of the repository totals, using the post-processed Cobertura through a SCRATCH helper. `BreadcrumbHtmlRenderer.cs` is not touched, so it is not in the floor list. No exclusion attribute or `coverage.config` entry is added, moved or removed (P0-T5, P1-T3, P5-T9, P5-T10 count it at 5). +- Mirror parity: not applicable. No CI job, required check, tool route or batch-budget fact in the execution-environment brief changes, and no mirrored `.claude`, `.agents` or bundle file is edited. +- File size: P0-T2, P3-T5, P4-T6 and P5-T8 measure physical lines of every edited `.cs` file after formatting; the largest edit target is 343 lines before an addition of roughly 25 lines. No near-limit test file named in the spec Constraints is touched, and no new file is created. +- Test isolation: the new tests build their own strict-mock controller per test, hold no static state, create no temporary files and use no sleep, delay or wall-clock wait (P3-T1 observes the fault synchronously). The full test route (P5-T5) runs with the repository default parallel settings and is not worked around with `Workers=1`, `[DoNotParallelize]` or retries. The Jest, Python and Pester isolation runs are not applicable to this C#-only change. +- CI-only behavior: the new C# tests are text-contract and strict-mock tests that read no gitignored `artifacts/` state; `QuickFiler.Test` stays C# 7.3 compatible (no `#nullable enable`); `git grep` is used only on tracked files; the spec amendment is a docs-only edit, but the code edits make the change a full CI run. +- Evidence: every artifact is a Markdown projection under `/evidence//` with `Timestamp:`, `Command:`, `EXIT_CODE:` and `Output Summary:`; consumers select the greatest `.*.md`; P3-T2 and P4-T2 are the expect-fail proofs and P3-T1 is a real fail-before (assertion failure) with no timer; no raw trx, `.coverage` or Cobertura document and no host or scratchpad path is committed (`SCRATCH` is recorded as a token); `git status --porcelain` listings are evidence rather than a clean-tree assertion because the orchestrator owns the commit. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/research/2026-10-22T09-17-efc-qfc-folder-tree-selection-and-nesting-research.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/research/2026-10-22T09-17-efc-qfc-folder-tree-selection-and-nesting-research.md new file mode 100644 index 000000000..e7e68fa5d --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/research/2026-10-22T09-17-efc-qfc-folder-tree-selection-and-nesting-research.md @@ -0,0 +1,243 @@ + + +# Task Research Notes: EFC recursive subfolder selection, EFC trash selection, QFC descendant nesting (#1002) + +- Issue: #1002 +- Branch: bug/efc-qfc-folder-tree-selection-and-nesting-1002 +- Researcher: task-researcher (read-only; no source, configuration, or test changes; no commands executed) +- Timestamp note: the orchestrator renamed this file to the UTC clock reading `2026-10-22T09-17` (`date -u`) taken when the file was accepted; the researcher's estimate `2026-10-22T09-30` was later than the clock. + +## Orchestrator Decisions on Open Questions (2026-10-22) + +1. Trash acceptance wording: after the trash button is pressed, the trash entry is the selected row and `SelectedFolder` resolves to the trash entry. The prior selection is not asserted beyond that. +2. QFC nested affordance: the +/− follows the folder name, matching the placement on top-level rows. It is not flush right. +3. EFC keyboard scope: parity with #997 only. Descendant toggles are focusable and respond to Enter and Space, as the QFC toggles do. New arrow-key navigation into deeper levels is out of scope, as #997 excluded new keyboard features. +4. First-level `ExpandLeafAsync` stale-render exposure: in scope. It is in a file this item already changes and has the same root-cause class as the new descendant fetch, so the same stale-completion guard is applied to it. +- Line numbers below describe the session checkout at the time of reading. They are research evidence only; a plan must cite symbols, not lines (plan gate G11). + +## Summary of Root Causes + +| # | Defect | Root cause (verified) | +|---|---|---| +| 1 | EFC has no recursive subfolder selection | EFC does not use the #997 surface at all. It uses a separate generated-document surface (`QuickFiler.Controllers.BreadcrumbBridgeRouter` + `UtilitiesCS...BreadcrumbHtmlRenderer` + `BreadcrumbDocumentAssets.BridgeJs`). That surface stores only one flat list of first-level children as key-less `BreadcrumbSegment` objects, renders them without any expand affordance, and has no inbound message that can address a child below the first level. None of the #997 code is reachable from EFC. | +| 2 | EFC trash entry is created but not selected | `ActionDeleteAsync` -> `ApplyDeleteGesture` prepends the trash row and calls the fire-and-forget `BindFolderRows`; `BreadcrumbBridgeRouter.BindRowsAsync` then deliberately clears the selection (#499) and re-navigates the document with no selected row. No code path ever selects the trash row. `EfcDataModel` is not involved. | +| 3 | QFC nested expand affordance on the left; no per-level indent | `FolderBreadcrumb.js` `makeSubfolders` appends the toggle span before the name span, so the toggle is the first flex item. Indentation comes only from the fixed `.sub { padding: 1px 4px 1px 24px; }` rule; nested `.subs` containers have no padding or margin, so every level renders at the same 24px indent. Depth is not carried on `BreadcrumbDescendantNode` or the render DTO, but it does not need to be: `makeSubfolders` is already recursive and can pass a depth argument. | + +## Research Executed + +### Files read end-to-end (or the relevant region) + +- Feature: `issue.md`, `spec.md`, `plan.2026-10-22T05-08.md` (template, unfilled). +- #997 archive: `research/research.2026-10-18T00-45.md`; `evidence/regression-testing/issue-997-javascript-behavior.2026-10-18T01-53.md`. +- EFC: `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs`, `EfcFormController.Actions.cs`, `EfcFormController.EventHandlers.cs` (60-249), `EfcFormController.cs` (200-266), `BreadcrumbBridgeRouter.cs`, `BreadcrumbBridgeRouter.Selection.cs`, `BreadcrumbBridgeRouter.Arrows.cs`, `BreadcrumbOutboundQueue.cs`, `QuickFiler/Viewers/WebView2BreadcrumbHost.cs`. +- UtilitiesCS (EFC model): `BreadcrumbRow.cs`, `BreadcrumbSegment.cs`, `BreadcrumbHtmlRenderer.cs`, `BreadcrumbDocumentAssets.cs`, `BreadcrumbMessageCodec.cs`, grep of `BreadcrumbMessages.cs` and `BreadcrumbRowBuilder.cs`. +- UtilitiesCS (QFC model): `BreadcrumbDescendantNode.cs`, `FolderBreadcrumbBridgeRouter.Descendants.cs`, `BreadcrumbRenderProjection.cs` (60-289). +- QFC page: `QuickFiler/Resources/FolderBreadcrumb.js` (full), `FolderBreadcrumb.html` (85-177). +- Tests: `tests/quickfiler-breadcrumb/issue-997-browser-harness.mjs`, `issue-997-recursive.test.mjs`, `issue-997-source-loader.mjs`; `QuickFiler.Test/Viewers/FolderBreadcrumbRecursiveAssetIssue997Tests.cs`; `QuickFiler.Test/Controllers/EfcFormControllerTests.cs` (1-120, 355-434); `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererTests.cs` (70-99); greps across `*.Test/**` for pinned markup and JS. + +### Searches + +- `InitializeBreadcrumbPipeline|EnsureBreadcrumbPipeline` in `QuickFiler`: only `QfcItemController` calls it, so the EFC form never constructs the #997 pipeline (its embedded `ItemViewer` included). +- `Resources\.FolderBreadcrumb|FolderBreadcrumbBridgeRouter|BreadcrumbBridgeCoordinator|new BreadcrumbBridgeRouter` in `QuickFiler`: EFC constructs only `new BreadcrumbBridgeRouter(` (`EfcFormController.Breadcrumb.cs:80`). +- `InternalsVisibleTo` in `UtilitiesCS`: granted to `UtilitiesCS.Test`, `ToDoModel.Test`, `DynamicProxyGenAssembly2` (`UtilitiesCS/Properties/AssemblyInfo.cs:18-20`). Not granted to `QuickFiler`, so `internal BreadcrumbDescendantNode` is not visible to the EFC router. +- `BreadcrumbHtmlRenderer|BreadcrumbMessageCodec|new BreadcrumbRow(|BreadcrumbRowBuilder` in production code: the EFC model types are used only by the EFC router, EFC controller files, and `EfcSelectionGuard`; QFC files reference only the `BreadcrumbRowBuilder.BannerPrefix` constant (`BreadcrumbStateModel.Row.cs:393`, `FolderSuggestionTree.cs:198`). +- `quickfiler-breadcrumb|node --test|\.mjs` in `.github/**`, `package.json`, `.vscode/tasks.json`, `scripts/**`: no matches. The node `.mjs` suites are not run by TaskMaster CI. + +## Question 1: How EFC presents and selects folders, and the #997 gap + +### Current EFC surface (separate from QFC) + +- Wiring: `EfcFormController.ConfigureBreadcrumbControl` (`EfcFormController.Breadcrumb.cs:69-92`) creates `WebView2BreadcrumbHost` over `EfcViewer.BreadcrumbWebView` (`EfcViewer.cs:85`, alias of the Designer `FolderListBox` WebView2), an `OutlookFolderHierarchyProvider`, and `new BreadcrumbBridgeRouter(provider, host, BreadcrumbMessageCodec, BreadcrumbHtmlRenderer, BreadcrumbOutboundQueue)`. +- Document: `BreadcrumbHtmlRenderer.RenderDocument` inlines `BreadcrumbDocumentAssets.BaseCss`, a theme block, row fragments, and `BridgeJs` (`BreadcrumbHtmlRenderer.cs:32-52`). It is delivered by `NavigateToString` (`BreadcrumbBridgeRouter.Selection.cs:183-195`). +- Row model: `BreadcrumbRow` (`UtilitiesCS`) holds `_leafChildren` as `List` (`BreadcrumbRow.cs:37`). `BreadcrumbSegment` carries only `FullPath`, `DisplayName`, `HasSubfolders`; it has no provider key (`BreadcrumbSegment.cs:29-43`). +- Expansion: `leafExpandToggle` -> `HandleLeafToggleAsync` -> `ExpandLeafAsync` (`BreadcrumbBridgeRouter.Arrows.cs:17-40, 165-209`) calls `_provider.GetImmediateSubfoldersAsync(row.ActiveSegmentKey, ...)`, maps the result through `BreadcrumbRowBuilder.MapSegments` (which drops each child's `FolderTreeNodeKey`), stores it with `row.SetLeafChildren(mapped)`, posts `subfolderResult`, then posts a row render. +- Child rendering: `AppendChildren` (`BreadcrumbHtmlRenderer.cs:213-232`) emits a flat `
name
` per first-level child. There is no affordance, no node identity, and no nested container. +- Child selection: `BridgeJs` posts `renderedChildActivate {rowId, childIndex}` for `.child[data-child-index]` (`BreadcrumbDocumentAssets.cs:79-88`); the router's `ActivateChild` (`BreadcrumbBridgeRouter.Selection.cs:36-48`) calls `SelectHierarchyPath`, which converts the rooted hierarchy path to the archive-relative stem and commits it (`Selection.cs:121-151`). `SelectedFolderPath` is the controller's `SelectedFolder` (`EfcFormController.SetupAndProperties.cs:179-184`), which `ActionOkAsync` validates with `EfcSelectionGuard.IsValidFilingSelection` and files through `EfcHomeController.ExecuteMovesAsync` (`EfcFormController.Actions.cs:30-64`, `EfcHomeController.ExecuteMoves.cs:69`). +- Inbound contract: the codec accepts only seven types (`BreadcrumbMessageCodec.cs:132-141`), each addressed by `rowId` plus integer `segmentIndex`/`childIndex`; no string node identity field exists (`BreadcrumbMessageCodec.cs:100-129`). + +### #997 pieces present for QFC and absent for EFC + +| #997 capability (QFC location) | EFC equivalent | Status | +|---|---|---| +| Lazy keyed descendant tree `BreadcrumbDescendantNode` with `Identity`, `Segment` (keyed `FolderBreadcrumbSegment`), `Expanded`, `Loaded`, `RequestGeneration`, `Children`, `Find`, `Collapse`, `Attach` (`BreadcrumbDescendantNode.cs:9-71`) | `BreadcrumbRow._leafChildren` flat, key-less | Absent | +| Descendant toggle with stale-completion rejection: `FolderBreadcrumbBridgeRouter.ToggleDescendantAsync` + `FetchAndAttachSubfoldersAsync` + `IsCurrentRequest` (`FolderBreadcrumbBridgeRouter.Descendants.cs:19-51, 112-199`) | none | Absent | +| Descendant activation by stable identity: `ActivateSelectorDescendant(rowIdentity, nodeIdentity)` (`Descendants.cs:13-16`) | `renderedChildActivate` by first-level integer index only | Absent below level 1 | +| Selected-descendant tracking and highlight (`BreadcrumbRenderProjection.ProjectDescendant`, `selected` flag, `selectedDescendantIdentity` in the page) | Row-level `_selectedRowId` only | Absent | +| Page: per-child `+/−` toggle posting `selectorDescendantToggle`, recursive `makeSubfolders`, activation posting `selectorDescendantActivate` (`FolderBreadcrumb.js:145-203`) | `BridgeJs` has one row-level `.affordance` handler and a flat `.child` handler | Absent | +| Coordinator dispatch (`BreadcrumbBridgeCoordinator.Descendants.cs`) and wiring (`QfcItemController.BreadcrumbWiring.cs`, `ItemViewer.Breadcrumb.cs`) | EFC router subscribes to `IBreadcrumbWebHost.MessageReceived` directly (`BreadcrumbBridgeRouter.cs:62, 446-457`) | Not applicable to EFC; no coordinator layer exists or is needed | + +The QFC filing-value fix of #997 (archive-relative stem for child selections) is already present in EFC through `SelectHierarchyPath`, so EFC needs only the tree, the addressing, and the rendering. + +### Candidate approaches + +**A. Extend the EFC surface with a keyed descendant tree (recommended).** +Keep the EFC router, renderer, codec, and `BridgeJs`. Store keyed descendants on `BreadcrumbRow` by reusing the existing `internal BreadcrumbDescendantNode` from inside `UtilitiesCS` (same assembly, so no visibility change). Add two inbound types addressed by `rowId` plus a string node identity. Render nested children recursively with the affordance after the name and one indent step per level. + +- Advantages: smallest change set; preserves every EFC-specific behavior (banner rows, trash pseudo-row, #440 arrow transitions, #799 absence suppression, #995 Enter confirmation, #614 archive-root validation); filing still flows through `SelectHierarchyPath`; reuses the #997 node type and its stale-request discipline; no QFC file changes. +- Limitations: EFC's inline JS is a C# string constant, so its click handlers can be verified only by text-contract tests (the existing EFC pattern), not by the node harness. + +**B. Replace the EFC surface with the QFC `FolderBreadcrumb.html` selector and `FolderBreadcrumbBridgeRouter`.** +- Rejected: the QFC selector is a collapsed/popup listbox with its own state model and keyboard contract; it has no banner rows, trash pseudo-row, segment activation, #440 tree transitions, or #995 `rowConfirmed` path. Porting EFC onto it is a rewrite of the EFC folder area and would put QFC-shared files in the change set, contrary to the minimal-fix rule. + +**C. Index-path addressing (for example `"0/2/1"`) on the existing flat model.** +- Rejected: an index path is invalidated by any re-fetch or rebind (row ids are `row-` and are reused across binds), which reintroduces the stale-address defects #997 removed with stable identities. + +### Recommended design for defect 1 (EFC) + +State model (UtilitiesCS, new partial file of `BreadcrumbRow`; `BreadcrumbRow.cs` is 361 lines and gains only the `partial` modifier and the reset calls): + +- `BreadcrumbRow` owns an optional descendant root `BreadcrumbDescendantNode` built from the active segment's key when first-level children are supplied as keyed `FolderBreadcrumbSegment`s. The existing `SetLeafChildren(IEnumerable)` stays unchanged for compatibility (existing tests call it with key-less segments; those rows simply have no expandable descendants). +- New public members (names are proposals): `SetLeafDescendants(IReadOnlyList)` (sets `_leafChildren` to the mapped segments and builds the keyed tree); `TryBeginDescendantExpand(string nodeId, out FolderTreeNodeKey key, out int generation)`; `bool TryCollapseDescendant(string nodeId)`; `bool TryAttachDescendantChildren(string nodeId, int generation, IReadOnlyList)` (rejects when the node is no longer in the tree, no longer expanded, or the generation moved); `string? GetDescendantPath(string nodeId)`; `string? SelectedDescendantId` with a setter method that validates the identity. +- `ActivateSegment`, `CollapseAfter`, `LeftArrow`, and leaf collapse must clear the descendant tree and `SelectedDescendantId` exactly where they already clear `_leafChildren` (`BreadcrumbRow.cs:169-170, 227, 300`). + +Router (QuickFiler, new partial `BreadcrumbBridgeRouter.Descendants.cs`): + +- `ExpandLeafAsync` passes the provider's keyed `children` to `row.SetLeafDescendants` instead of mapping and discarding keys (`Arrows.cs:185-190`). +- `descendantToggle {rowId, nodeId}`: collapse when expanded and post a row render; otherwise begin expand, `await _provider.GetImmediateSubfoldersAsync(key, CancellationToken.None)` (no `ConfigureAwait(false)`, matching `ExpandLeafAsync`), then attach only when `ReferenceEquals(FindRow(row.RowId), row)` and `TryAttachDescendantChildren` succeeds; post one row render (no `subfolderResult`, mirroring #997 where descendant expansion returns only a render, `FolderBreadcrumbBridgeRouter.Descendants.cs:169-172`). Provider faults and cancellation are logged and leave state unchanged, as in `ExpandLeafAsync`. +- `descendantActivate {rowId, nodeId}`: resolve the path with `GetDescendantPath`; reject unknown identities with a log line; otherwise set `SelectedDescendantId` and call the existing `SelectHierarchyPath(row, path)`, which enforces the archive-relative contract and raises `SelectedFolderPathChanged`. `ConfirmRow` already accepts it because `_selectedRowId` becomes the containing row (`Selection.cs:153-166`). +- `ProcessInboundAsync` (`BreadcrumbBridgeRouter.cs:395-443`) gains two `case` arms that delegate to the new partial; `BreadcrumbBridgeRouter.cs` is 459 lines, so the arms must stay one-line delegations. + +Codec and messages (UtilitiesCS): add `DescendantToggle = "descendantToggle"` and `DescendantActivate = "descendantActivate"` to `BreadcrumbMessageTypes`, an optional `NodeId` string on `BreadcrumbInboundMessage` (optional constructor parameter, source-compatible), registration in `IsKnownInboundType`, and a required non-empty `nodeId` check for both types, failing fast like the existing `childIndex` check. + +Renderer and assets (UtilitiesCS): + +- `AppendChildren` renders recursively from the descendant tree when one exists, otherwise exactly as today. Each child: `
` (first-level children keep the `class="child" data-child-index="0"` prefix pinned by `BreadcrumbHtmlRendererTests.cs:89`), a name span, then, only when `HasChildren`, `+` or `−`. An expanded node is followed by a nested `
`; `.children { margin-left: 18px; }` (`BreadcrumbDocumentAssets.cs:28`) then compounds per level without a CSS change. Mark the selected descendant with an attribute (for example `aria-selected="true"`) rather than a second class so the pinned prefix survives. +- `BridgeJs` click handler: add a `.affordance[data-role="node"]` branch that posts `descendantToggle` and stops propagation, placed BEFORE the `.child` branch (the affordance sits inside the `.child` element, and the current `.child` branch at `BreadcrumbDocumentAssets.cs:79-88` would otherwise swallow it); add a `.child[data-node-id]` branch posting `descendantActivate`; keep the existing `renderedChildActivate` branch for key-less children (pinned by `BreadcrumbHtmlRendererTests.cs:91`). Add `.child { display: flex; align-items: center; }` so the affordance renders to the right of the name. + +## Question 2: EFC trash button + +### Current behavior + +1. `BtnDelItem.Click` -> `ButtonDelete_Click` -> `ButtonDeleteClickAsync` -> `ActionDeleteAsync` (`EfcFormController.EventHandlers.cs:71, 204-217`); the `T` key routes to the same method (`EventHandlers.cs:278, 350`). +2. `ActionDeleteAsync` awaits `_formViewer.UiSyncContext`, then calls synchronous `ApplyDeleteGesture` (`EfcFormController.Actions.cs:95-105`). +3. `ApplyDeleteGesture` sets `_folderRows = WithTrashRow(_folderRows)` (prepends `"Trash to Delete"` idempotently, `Actions.cs:79-92`) and calls `BindFolderRows`, which discards the task of `BindBreadcrumbRowsAsync` (`EfcFormController.Breadcrumb.cs:163-172`). +4. `BreadcrumbBridgeRouter.BindRowsAsync` rebuilds rows (the trash text is classified `TrashPseudoRow`, `BreadcrumbRowBuilder.cs:164-166`), sets `_selectedRowId = null`, nulls `SelectedFolderPath` and raises `SelectedFolderPathChanged(null)` when a selection existed (#499), then `DeliverDocument` navigates a document with no selected row (`BreadcrumbBridgeRouter.cs:157-179`). +5. Nothing selects the new row. `EfcDataModel` is not mutated by the gesture; its only trash reference is the attachment decision during a move (`EfcDataModel.cs:281`). + +Observation: per the code, the router clears the previous selection rather than keeping it, so after the gesture `SelectedFolder` is null and OK reports "Please select a valid folder." The maintainer's description ("the selection stays where it was") could not be reconciled with the code by reading alone; either way the required outcome is the same. ORCHESTRATOR/MAINTAINER TO CONFIRM if the distinction matters for the acceptance wording. + +The QFC analogue does select it: `QfcItemController.MailActions.cs:239-243` adds the trash item if missing and calls `SetFolderSelectedItem("Trash to Delete")`. + +### Why "select after bind" is unsafe + +Selecting with the existing `SelectRow` after the bind would commit state correctly but would deliver the highlight as a posted `render` message (`CommitSelection`, `Selection.cs:143-151`) immediately after `NavigateToString` replaced the document. `WebView2BreadcrumbHost` posts with `PostWebMessageAsJson` to whatever document is current (`WebView2BreadcrumbHost.cs:210-248`) and nothing waits for navigation completion; `BreadcrumbOutboundQueue` gates only on core initialization (`BreadcrumbOutboundQueue.cs:37-52`). The posted render can therefore reach the outgoing document or arrive before the new page has registered its listener, leaving the highlight missing even though `SelectedFolder` is correct. This is an inference from the WebView2 delivery model; it is not reproducible in a unit test. + +### Recommended fix for defect 2 + +Make the selection part of the bound document: + +- Router: add an internal `BindRowsAsync` overload with an optional `string? preferredRowText`. After rows are rebuilt and the #499 clear has run, if a row whose presented text equals `preferredRowText` exists (for the trash text: `Kind == TrashPseudoRow`), set `_selectedRowId` and `SelectedFolderPath` through the same normalization as `SelectRow` and raise `SelectedFolderPathChanged` once, before `DeliverDocument`, so `RenderDocument(_rows, _darkMode, _selectedRowId)` emits `rowwrap selected` for that row. The existing four-argument overload delegates with `null`, so every current caller is unchanged. Put the helper in `BreadcrumbBridgeRouter.Selection.cs` (236 lines) to keep `BreadcrumbBridgeRouter.cs` (459 lines) under the limit. +- Controller: replace the synchronous `ApplyDeleteGesture` with an awaited form: set `_folderRows = WithTrashRow(_folderRows)`; return when `_formViewer` or `_router` is null (preserves the guard the existing `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows` test relies on, `EfcFormControllerTests.cs:385-416`); otherwise `await` a bind that passes `TrashRowText` as the preferred row. `ActionDeleteAsync` awaits it. + +Rejected alternative: post-bind `SelectRow` (navigation race above). + +## Question 3: QFC descendant row layout + +### Evidence + +- DOM order: in `makeSubfolders` the toggle is appended at `FolderBreadcrumb.js:177` and the name at `:179`, so for a child with `hasChildren` the `.sub` flex row is `[toggle][name]`. No CSS `order`, `float`, or `row-reverse` is involved; `.sub` is `display: flex` (`FolderBreadcrumb.html:151-157`). +- Top-level parity: the top-level leaf affordance is projected as a cell after the terminal segment (`BreadcrumbRenderProjection.cs:230-241`) and rendered in cell order (`FolderBreadcrumb.js:134`), so on top-level rows the `+/−` follows the name. (The collapsed-chain re-expand plus deliberately sits left of its segment, `BreadcrumbRenderProjection.cs:217-223`; it is unrelated to descendant rows.) +- Indent: nested lists are appended as `.subs` inside the parent `.subs` (`FolderBreadcrumb.js:198-200`), but `.subs` carries only a background (`FolderBreadcrumb.html:150`) and every `.sub` has the same fixed `padding-left: 24px` (`:155`). All levels therefore share one indent. +- Depth availability: `BreadcrumbDescendantNode` has no depth (`BreadcrumbDescendantNode.cs:9-71`) and `BreadcrumbSubfolderRender` has none (`BreadcrumbRenderProjection.cs:60-91`). Depth is implicit in the recursive `Children` nesting, which the page already walks recursively; no C# change is required. + +### Recommended fix for defect 3 (JS only) + +- In `makeSubfolders`, append `name` first, then the toggle, so the affordance is to the right of the name at every level, matching the top-level cell order. +- Add a `depth` parameter (top-level call in `render` passes `0`; the recursive call at `FolderBreadcrumb.js:199` passes `depth + 1`). On each `.sub`, set `data-depth` and an explicit left padding of `24 + 16 * depth` pixels through `setAttribute("style", "padding-left: ...px")` (the harness `Element` has `setAttribute` but no `style` object, so this form needs no harness change). The full-width highlight of `.sub.selected` is preserved because the indent is padding on the row itself. +- Keep the recursive call text `makeSubfolders(row, child.children` so the pins in `FolderBreadcrumbRecursiveAssetIssue997Tests.cs:18` and `FolderBreadcrumbAssetContractTests.cs:357` still match, and add no function between `makeSubfolders` and `render` (`FolderBreadcrumbAssetContractTests.cs:317-318` slices bodies by adjacent function names). + +Rejected alternative: CSS-only `.subs .subs { padding-left: 16px; }`. It indents correctly, but it indents the selection highlight too, and a node-harness test cannot observe CSS, so the fix would be verifiable only by a text assertion on the stylesheet. + +Open interpretation: "on the right" is taken as "immediately after the name", matching top-level rows, not "flush with the right edge". MAINTAINER TO CONFIRM if right-edge alignment is intended (that would add `margin-left: auto` on the descendant toggle). + +## Question 4: Test seams and locations + +All proposed tests are in-memory: Moq strict mocks of `IFolderHierarchyProvider` and `IBreadcrumbWebHost`, captured `NavigateToString`/`PostMessageJson` strings, `TaskCompletionSource` barriers for ordering, and the existing node DOM harness. No temporary files. + +| Defect | Seam | Existing pattern | Proposed new test file | +|---|---|---|---| +| 1 (row state) | `BreadcrumbRow` public API with `FolderBreadcrumbSegment` fixtures | `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowStateTests.cs` (379 lines) | `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` | +| 1 (codec) | `BreadcrumbMessageCodec.DeserializeInbound` | `BreadcrumbMessageCodecTests.cs` (309 lines) | `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` | +| 1 (renderer, assets) | `BreadcrumbHtmlRenderer.RenderRowFragment`, `BreadcrumbDocumentAssets.BridgeJs` text | `BreadcrumbHtmlRendererTests.cs` (363 lines; Issue439/Issue995 contract style) | `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` | +| 1 (router) | Real `BreadcrumbBridgeRouter` over mocked provider/host, inbound JSON via `ProcessInboundAsync` | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue995Tests.cs` (197), `...Issue439Tests.Activation.cs` (253) | `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` | +| 2 (router) | `BindRowsAsync` overload with preferred row; assert `SelectedFolderPath`, one changed event, and `rowwrap selected` in the navigated document | same router tests | in `BreadcrumbBridgeRouterIssue1002Tests.cs` (or a `.Trash.cs` sibling if the file grows) | +| 2 (controller) | `ActionDeleteAsync` on the private-constructor controller with `_formViewer` (uninitialized `EfcViewer` plus `SynchronizationContext`), `_router` (real router, strict mocks), `_globals` (mock `IOlObjects.ArchiveRootPath`) injected by reflection | `EfcFormControllerTests.Issue439BindBreadcrumbRowsAsync_SubmitsArchiveRootToRealRouter` and `ActionDeleteAsync_AwaitedTwice...` | new partial `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` (`EfcFormControllerTests.cs` is 485 and `.Part2.cs` 490 lines) | +| 3 (JS behavior) | `createBrowser(source)` DOM harness running the real `FolderBreadcrumb.js` | `tests/quickfiler-breadcrumb/issue-997-recursive.test.mjs` | `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs` | +| 3 (CI-visible contract) | `FolderBreadcrumbDocument.Html` text | `FolderBreadcrumbRecursiveAssetIssue997Tests.cs` | `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` | + +Fail-before candidates (each fails on the current tree): + +- Router: `descendantToggle` on a second-level node calls `GetImmediateSubfoldersAsync` with that node's key and the next row render contains its children (today the codec throws `Unknown inbound breadcrumb message type`). +- Router: `descendantActivate` on a grandchild sets `SelectedFolderPath` to the archive-relative stem (for example `Clients\North\Q3\Invoices`) and `rowConfirmed` then raises `ConfirmationRequested` once. +- Router: a toggle whose provider task is completed after the node was collapsed (or after a rebind) attaches nothing and posts no render for the replaced row. +- Renderer: a keyed child with `HasChildren` renders `data-role="node"` after its name; a leaf child renders none; nested children sit in a nested `.children` container. +- Trash: after `await ActionDeleteAsync()`, `SelectedFolder` equals `"Trash to Delete"` and the last navigated document marks the trash row `rowwrap selected`. +- JS: for every `.sub` with a toggle, the toggle's child index is greater than the name's; `.sub` elements at levels 0/1/2 carry `data-depth` `0`/`1`/`2` and increasing `padding-left`. +- C# contract: inside the `makeSubfolders` body, `appendChild(name)` precedes `appendChild(toggle)`, and the recursive call passes `depth + 1`. + +Existing tests to keep green (no expected change): `BreadcrumbHtmlRendererTests` (pins `class="child" data-child-index="0"`, `type: 'renderedChildActivate'`, `e.stopPropagation();`), `BreadcrumbBridgeRouterTests` and `...QueueTests` (pin `subfolderResult` on first-level expansion), `FolderBreadcrumbAssetContractTests` (pins `makeSubfolders(row, child.children`, `subfolderElement.*` attribute lines), `issue-997-recursive.test.mjs` (order-independent `.aff`/`.seg` queries), `EfcFormControllerTests.ActionDeleteAsync_AwaitedTwice...` (requires the null-router guard to remain). + +## Question 5: Proposed file inventory + +Physical line counts are from reading or a line-count search on the session checkout. + +Production: + +| File | Lines now | Change | Size note | +|---|---|---|---| +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` | 361 | add `partial`; clear descendant state in the three existing reset points | ok | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` | new | keyed tree members (reuses `BreadcrumbDescendantNode`) | new | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` | 234 | recursive children, node affordance after name, selected marker | ok | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` | 149 | `.child` flex CSS; node-affordance and descendant-activate branches ordered before `.child` | ok | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` | 173 | two type constants; optional `NodeId` | ok | +| `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` | 203 | register types; require `nodeId` | ok | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` | 459 | two delegating `case` arms; optional preferred-row parameter threaded into the existing bind | FLAG: near 500; add no logic here | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` | new | toggle/activate handlers with stale guard | new | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` | 211 | `ExpandLeafAsync` keeps keyed children | ok | +| `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` | 236 | preferred-row selection helper | ok | +| `QuickFiler/Controllers/EfcFormController.Actions.cs` | 184 | awaited delete gesture with trash preselection | ok | +| `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` | 208 | bind entry point accepting the preferred row | ok | +| `QuickFiler/Resources/FolderBreadcrumb.js` | 347 | name-then-toggle order; depth parameter and indent | ok | +| `UtilitiesCS/UtilitiesCS.csproj`, `QuickFiler/QuickFiler.csproj` | n/a | explicit `` for the two new partial files (legacy non-SDK projects) | ORCHESTRATOR TO CONFIRM item placement | + +No change: `FolderBreadcrumb.html` (177), `BreadcrumbDescendantNode.cs` (72), all `FolderBreadcrumbBridgeRouter*`/`BreadcrumbSelectionSession*`/`BreadcrumbBridgeCoordinator*` files, `EfcDataModel.cs` (485, FLAG near limit, untouched). + +Tests (new files; `QuickFiler.Test.csproj` and `UtilitiesCS.Test.csproj` need `` entries if they are non-SDK, ORCHESTRATOR TO CONFIRM): the seven files in the Question 4 table. Near-limit test files that must not be extended: `EfcFormControllerTests.cs` (485), `EfcFormControllerTests.Part2.cs` (490), `BreadcrumbBridgeRouterTests.cs` (462), `BreadcrumbBridgeRouterQueueTests.cs` (462), `BreadcrumbBridgeRouterScoreJoinTests.cs` (463), `BreadcrumbBridgeRouterIssue439Tests.cs` (455), `FolderBreadcrumbAssetContractTests.cs` (415). + +## Question 6: Risks + +- Shared code with QFC: the EFC model types (`BreadcrumbRow`, `BreadcrumbHtmlRenderer`, `BreadcrumbDocumentAssets`, `BreadcrumbMessageCodec`, `BreadcrumbMessages`) are not used by the QFC path except for the `BannerPrefix` constant, so the EFC change cannot alter QFC behavior. The one shared type reused, `BreadcrumbDescendantNode`, must be consumed unchanged; any change to it reaches `FolderBreadcrumbBridgeRouter` and `BreadcrumbRenderProjection`. +- QFC JS change: `FolderBreadcrumb.js` is the single page for both the collapsed and popup QFC surfaces; the change must not touch `render`, `makeRow`, or the message listener. Existing pinned strings listed in Question 4 constrain the edit. +- Thread affinity (EFC): the EFC router has no lock and relies on running on the UI context: inbound messages arrive on the WebView2 event (UI thread), and `ExpandLeafAsync`/`FetchChainAsync` await without `ConfigureAwait(false)`, so continuations resume on the captured UI context. The new descendant handler must follow the same rule. Copying the QFC router's `ConfigureAwait(false)` plus `lock (_sync)` model would mutate EFC row state on a pool thread concurrently with UI handlers. +- Stale completions (EFC): row ids are positional (`row-`) and reused across binds, so a descendant fetch that completes after a rebind (for example a trash gesture during a pending expand) could post a fragment that replaces the new row with the same id. The new handler must check `ReferenceEquals(FindRow(row.RowId), row)` and the node generation before attaching or rendering. The existing first-level `ExpandLeafAsync` has the same latent exposure; it is out of the stated defect scope and is recorded here so the planner can decide whether to remediate it in place. +- Trash ordering: awaiting the bind makes the delete gesture wait for provider chain lookups of every suggestion row. The current gesture already triggers those lookups (fire-and-forget); awaiting adds no work, but an exception path is now observed by `ButtonDeleteClickAsync`'s boundary instead of being reported by `BindBreadcrumbRowsAsync`'s own boundary. Keeping the bind call inside `BindBreadcrumbRowsAsync` (which catches and reports) preserves the current reporting. +- `subfolderResult` JS: `BridgeJs` renders `subfolderResult` children without `data-child-index` (`BreadcrumbDocumentAssets.cs:131-142`); the subsequent row render replaces them. Descendant expansion must post only the row render so key-less transient children are never shown at deeper levels. + +## Automation Feasibility + +- Manual Outlook verification (open EFC with a three-level folder tree; expand and select a grandchild; press OK and confirm the filing destination; press the trash button and observe the highlight; open QFC and inspect the affordance side and per-level indent) is a human-interaction step. It cannot be automated in this repository: it requires a live Outlook process, a mailbox, and the WebView2 runtime. +- Verifiable by unit tests instead: + - Defect 1: codec acceptance and rejection of the new types; row-state transitions (expand, collapse, attach with matching and stale generations, unknown identity); router end to end over mocked provider and host (provider called with the descendant key, render content, archive-relative `SelectedFolderPath`, one change event, Enter confirmation, stale-completion rejection); rendered markup (affordance after name, nesting, encoding); `BridgeJs` text contract (branch order, message shapes). + - Defect 2: router preselection (state, single event, `rowwrap selected` in the navigated document) and controller `ActionDeleteAsync` end to end with a real router. + - Defect 3: node harness assertions on DOM order and depth attributes against the real `FolderBreadcrumb.js`, plus a CI-visible C# text contract. +- Not verifiable by unit tests: actual pixel layout, WebView2 message delivery timing across `NavigateToString`, and the EFC `BridgeJs` click behavior in a browser engine (it is a C# string constant with no node harness). + +## Pre-Mortem + +- Review checklists: a reviewer can flag the 500-line limit on `BreadcrumbBridgeRouter.cs` (459 lines) and the near-limit test files, and a missing fail-before artifact per defect. Mitigation: plan only delegating arms in the main router file, put all new logic in new partial files, add tests only in new files, and record fail-before/pass-after runs under `evidence/regression-testing/` for each defect. +- Coverage: new members must reach at least 90% and changed lines must not lose coverage (CLAUDE.md floors 80% line / 75% branch repository-wide). The EFC router, row, renderer, and codec are measured (not coverage-exempt); the JS file is not measured by the C# coverage tool. Mitigation: router and row tests cover every rejection branch (unknown row, unknown node, leaf node, stale generation, replaced row, provider fault, cancellation); record the node `--experimental-test-coverage` figure for `FolderBreadcrumb.js` as #997 did. +- Mirror parity: not applicable; no `.claude`, `.agents`, `.codex`, or bundled resource file is changed, and the execution-environment brief's facts are unchanged. +- File size: `BreadcrumbBridgeRouter.cs` 459, `EfcDataModel.cs` 485, `EfcFormControllerTests.cs` 485, `EfcFormControllerTests.Part2.cs` 490, `BreadcrumbBridgeRouterTests.cs` 462, `...QueueTests.cs` 462, `...ScoreJoinTests.cs` 463, `...Issue439Tests.cs` 455. Mitigation: none of these receives more than a few delegating lines; new partial files carry the logic; verify physical counts with a whole-file line count after edits. +- Test isolation: tests must create no files and share no static state; the router and controller tests construct fresh instances per test. MSTest runs in parallel at class level (`Workers=0`), so no test may rely on static state in `WebView2BreadcrumbHost._owners` (avoid constructing real hosts). Mitigation: strict Moq host mocks only; for JS, run the new `.mjs` file with the #997 suites in normal, reversed, and shuffled order as #997 recorded. +- CI-only behavior: TaskMaster CI (actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, pester) does not run the node `.mjs` suites, so the JS behavioral tests give no CI signal. Mitigation: pair each JS behavior with a C# text-contract test in `QuickFiler.Test` that runs under `mstest-coverage`; keep new C# files under `#nullable enable` consistent with neighbors so `build-nullable` checks them; run CSharpier on the new files (format-check). +- Evidence: baseline, fail-before, pass-after, final QA, and coverage-comparison artifacts are required under `/evidence//` with `Timestamp`, `Command`, `EXIT_CODE`, `Output Summary`, no host paths, and `SCRATCH` for scratch locations. The manual Outlook step must be recorded as a human-interaction step, not claimed as executed. Mitigation: the plan names each artifact path per task. + +## Open Questions + +1. Trash: the code clears the prior selection on rebind; the maintainer reports it "stays where it was". Confirm whether the acceptance criterion should state only "the trash entry is selected and `SelectedFolder` is `Trash to Delete`". +2. QFC affordance placement: immediately after the name (recommended, matches top-level rows) or flush right. +3. EFC keyboard navigation into deeper descendants (Right/Left/Up/Down across nested children) is not requested and is not in the recommendation. Confirm it is out of scope. +4. Whether the latent stale-render exposure in the existing first-level `ExpandLeafAsync` should be remediated in this item (related root cause) or left as is. +5. Orchestrator to confirm: the exact research filename stamp, and `` placement for new files in the legacy project files. + +No numeric acceptance-criterion count is proposed by this research. diff --git a/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md new file mode 100644 index 000000000..5d19d079a --- /dev/null +++ b/docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/spec.md @@ -0,0 +1,358 @@ +# 2026-10-22-efc-qfc-folder-tree-selection-and-nesting (Spec) + +- **Issue:** #1002 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-10-22 +- **Status:** Ready for planning +- **Version:** 0.2 +- **Work Mode:** full-bug. This file is the sole authoritative acceptance-criteria source (`acceptance-criteria-tracking`: full-bug resolves to spec.md only). No user-story.md is produced for this item. + +> Authoring conventions (do not "fix" these): +> - Every file this fix creates or modifies is listed in `## Write Set` as a backticked repository-relative path and may be backticked elsewhere. Files that are cited for comparison but must NOT change are written as plain prose without backticks, so the footprint harvester does not read them as write targets. +> - Acceptance-criterion lines contain no digits, no percent signs and no angle brackets. Counts and figures are written as words; evidence artifacts are named on AC lines by evidence kind and file stem only, with the full feature-relative path given in `## Test Strategy`. The research record supplies no `## Numeric Derivation Evidence`, so no AC asserts a derived count. +> - Line numbers cited below describe the checkout at research time and are evidence only; the plan must cite symbols, not lines. + +## Context + +Three defects in the QuickFiler folder-selection surfaces: + +1. **EFC has no recursive subfolder selection.** The EFC viewer cannot expand and select a subfolder below the first revealed level, which the QFC viewer gained in issue #997 (PR #998). +2. **EFC trash button does not select the trash entry.** Pressing the trash button prepends the "Trash to Delete" row, but no row is selected afterwards. +3. **QFC nested rows are laid out wrongly.** In the QFC selector, a descendant row that has its own subfolders shows its +/− before the folder name instead of after it, and every nested level shares the same indent. + +Environment: Windows 11, Outlook desktop (VSTO add-in), WebView2 breadcrumb surfaces; C# on .NET Framework 4.8 plus the QFC JavaScript asset. Reproduction requires a mailbox whose folder tree has at least three nested levels. + +Impact / Severity: Medium. Logs / Screenshots: none attached; reported by the maintainer on 2026-10-22 with no log excerpt. + +Research: research/2026-10-22T09-17-efc-qfc-folder-tree-selection-and-nesting-research.md (this folder). Its "Orchestrator Decisions on Open Questions" section is binding and is applied throughout this spec: + +- D1 (trash wording): after the trash button is pressed, the trash entry is the selected row and `SelectedFolder` resolves to it. The prior selection is not asserted beyond that. +- D2 (QFC affordance placement): the +/− immediately follows the folder name, as on top-level rows. It is not flush right. +- D3 (EFC keyboard scope): parity with #997 only. Descendant toggles are focusable and respond to Enter and Space. No new arrow-key navigation into deeper levels. +- D4 (first-level stale render): in scope. The same stale-completion guard is applied to the existing first-level `ExpandLeafAsync`. + +Design precedent: docs/features/archive/2026-10-17-quickfiler-recursive-folder-selection-997/spec.md. + +## Repro & Evidence + +Steps to Reproduce: +1. Open the EFC viewer, open the folder suggestions, and try to drill into and select a subfolder of a suggested folder. +2. In the EFC viewer, press the trash button. +3. Open the QFC viewer, expand a suggested folder, then look at a child subfolder that itself has subfolders and expand it. + +Expected: +1. EFC offers the same recursive subfolder expansion and selection that QFC gained in #997, and the selected subfolder becomes the filing destination. +2. After the trash button adds the trash entry, that entry is the selected entry. +3. In QFC, the expand affordance of every expandable subfolder row follows the folder name, matching the top-level rows, and each nested level is indented one step further than its visible parent. + +Actual: +1. EFC does not offer recursive subfolder selection. +2. The trash entry is created but is not selected. +3. The expand affordance of a nested subfolder renders before the name, and nested levels share one indent. + +Note on defect 2: the maintainer reported that "the selection stays where it was". Reading the code shows the router clears the prior selection on rebind (#499), so after the gesture `SelectedFolder` is null and OK reports "Please select a valid folder." Per D1 the required outcome is identical either way, and no criterion asserts the prior selection. + +Fail-before candidates (each fails on the current tree; see Test Strategy for test names): +- EFC: a `descendantToggle` inbound message is rejected by the codec as an unknown type. +- EFC: after `await ActionDeleteAsync()`, `SelectedFolder` is null and the navigated document contains no `rowwrap selected` row. +- QFC: inside `makeSubfolders`, `subfolderElement.appendChild(toggle)` precedes `subfolderElement.appendChild(name)`, and the recursive call carries no depth. + +## Scope & Non-Goals + +- In scope: + - Defect 1: EFC keyed descendant tree on the EFC row model, identity-addressed `descendantToggle` / `descendantActivate` inbound messages, recursive EFC rendering with the affordance after the name, stale-completion guards on the new descendant fetch AND on the existing first-level `ExpandLeafAsync` (D4), and filing of the selected descendant through the existing `SelectHierarchyPath` archive-relative contract. + - Defect 2: EFC trash preselection delivered inside the same bound document through a preferred-row parameter on the router bind, with the delete gesture awaiting that bind. + - Defect 3: QFC `makeSubfolders` DOM order (name, then toggle) and a per-depth indent of one step of 16px per level, in the JavaScript asset only. +- Out of scope / non-goals: + - No new arrow-key navigation (Left/Right/Up/Down) into or across EFC descendants (D3; #997 likewise excluded new keyboard features). Existing EFC arrow transitions (#440) are unchanged. + - No keyboard activation of EFC descendant rows beyond the toggle parity in D3; Enter outside a focused descendant toggle keeps the existing row-confirmation behavior (#995). + - No changes to the QFC router, selection session, coordinator, render projection, or QFC wiring. The QFC fix is JavaScript-only. + - No move of the EFC folder area onto the QFC selector document or router (research approach B, rejected). + - No index-path addressing of descendants (research approach C, rejected). + - No flush-right placement of the QFC affordance (D2). + - No change to how the trash row is filed, moved, or deleted; only its selection after the gesture. +- Explicitly excluded files (deliberately unbackticked; they must not appear in the diff): UtilitiesCS/OutlookObjects/Folder/BreadcrumbDescendantNode.cs (reused unchanged), UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.cs and every FolderBreadcrumbBridgeRouter partial, every BreadcrumbSelectionSession file, UtilitiesCS/OutlookObjects/Folder/BreadcrumbRenderProjection.cs, every QuickFiler/Viewers/BreadcrumbBridgeCoordinator file, QuickFiler/Viewers/ItemViewer.Breadcrumb.cs, QuickFiler/Controllers/QfcItemController.BreadcrumbWiring.cs, QuickFiler/Resources/FolderBreadcrumb.html, QuickFiler/Properties/Resources.resx, QuickFiler/Controllers/EfcDataModel.cs. + +## Root Cause Analysis + +| # | Defect | Verified root cause | +|---|---|---| +| 1 | EFC has no recursive subfolder selection | EFC does not use the #997 surface. It uses its own generated-document surface: `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` with `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` and the `BridgeJs` constant in `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs`. `BreadcrumbRow` stores first-level children as a flat key-less `List`; `ExpandLeafAsync` maps the provider's keyed children through `BreadcrumbRowBuilder.MapSegments`, which discards each `FolderTreeNodeKey`. `AppendChildren` renders a flat `div.child` per first-level child with no affordance and no identity. The codec accepts only integer-indexed inbound types, so no message can address a node below the first level. | +| 2 | EFC trash entry created but not selected | `ActionDeleteAsync` calls synchronous `ApplyDeleteGesture`, which sets `_folderRows = WithTrashRow(_folderRows)` and calls `BindFolderRows`, which discards the task of `BindBreadcrumbRowsAsync`. `BreadcrumbBridgeRouter.BindRowsAsync` rebuilds rows, sets `_selectedRowId = null`, clears `SelectedFolderPath` (#499), and `DeliverDocument` navigates a document with no selected row. No code path selects the trash row. The EFC data model is not involved. | +| 3 | QFC nested affordance before the name; flat indent | In `QuickFiler/Resources/FolderBreadcrumb.js` `makeSubfolders`, `subfolderElement.appendChild(toggle)` runs before `subfolderElement.appendChild(name)`, so the toggle is the first flex item. Indent comes only from the fixed `.sub` rule `padding: 1px 4px 1px 24px` in FolderBreadcrumb.html; nested `.subs` containers carry no padding, so every level renders at the same indent. Depth is implicit in the recursive call and need not be added to any C# DTO. | + +Why "select after bind" is rejected for defect 2: calling the existing `SelectRow` after the bind commits state correctly but delivers the highlight as a posted `render` message immediately after `NavigateToString` replaced the document. The WebView2 host posts to whatever document is current and nothing waits for navigation completion, so the highlight can reach the outgoing document or arrive before the new listener is registered. This is an inference from the WebView2 delivery model and is not reproducible in a unit test; the fix therefore puts the selection inside the bound document. + +## Proposed Fix + +### Design summary (what changes where): + +- **Defect 1 (EFC recursive selection, research approach A).** Keep the EFC router, renderer, codec, and `BridgeJs`. Add a keyed descendant tree to `BreadcrumbRow` in a new partial file, reusing the existing internal `BreadcrumbDescendantNode` unchanged (same assembly, so no visibility change). Add two inbound types addressed by `rowId` plus a string `nodeId`. Render children recursively with the affordance after the name. Route both new messages through a new router partial with stale-completion guards, and file the activated descendant through the existing `SelectHierarchyPath`. +- **Defect 2 (EFC trash).** Add a preferred-row parameter to the router bind so the trash row is selected before `DeliverDocument`, and make the delete gesture await that bind. +- **Defect 3 (QFC nesting).** In `makeSubfolders`, append the name before the toggle and add a depth parameter that sets `data-depth` and an inline left padding per level. + +### Boundaries and invariants to preserve: + +Invariants this fix establishes, one sentence each: + +- **I1 (EFC descendant identity):** a descendant can be toggled or activated only by a `nodeId` that the router itself issued for a node currently present in the addressed row's tree; any other identity is logged and changes no state, raises no event, and calls no provider. +- **I2 (EFC stale completion):** a provider result is applied to a row only when, after the await, `FindRow(row.RowId)` returns the same row instance and (for descendants) the node is still in the tree, still expanded, and its `RequestGeneration` equals the generation captured at request time; otherwise nothing is attached and nothing is posted for that row id. This applies to the new descendant fetch and to the existing first-level `ExpandLeafAsync`. +- **I3 (EFC toggle versus selection):** a `descendantToggle` changes expansion only; it never changes `SelectedFolderPath` and never raises `SelectedFolderPathChanged`. +- **I4 (EFC filing value):** a `descendantActivate` commits only through `SelectHierarchyPath`, so the committed `SelectedFolderPath` is the archive-relative stem when an archive root is bound, and an out-of-root or archive-root target is a non-selection that leaves the prior selection and the rendered highlight unchanged (#614, #499). +- **I5 (EFC trash):** when the delete gesture completes, `SelectedFolderPath` equals "Trash to Delete" and the document delivered by that bind already marks the trash row `rowwrap selected`; no follow-up render message is used to establish the selection. +- **I6 (EFC thread affinity):** all EFC router state is mutated on the UI context. New awaits in router files do not use `ConfigureAwait(false)`, and no lock is introduced. +- **I7 (QFC layout):** in every `.sub` row rendered by `makeSubfolders`, the name element precedes the toggle element, and a row at depth d carries `data-depth` equal to d and a left padding of 24 + 16 × d pixels, so each nested level is exactly one 16px step further in than its visible parent. + +Trace for the EFC descendant path (defect 1), one accepted value from accept to commit: + +1. Accept point: `BridgeJs` posts `{ type: 'descendantActivate', rowId, nodeId }`. `BreadcrumbMessageCodec.DeserializeInbound` validates only shape (known type, string `rowId`, non-empty string `nodeId`); it does not validate that the node exists. +2. Row resolution: `ProcessInboundAsync` resolves the row with `FindRow`; an unknown row is logged and returns, as today. +3. Identity validation: the new router partial resolves the path with the row's `GetDescendantPath(nodeId)`; an unknown identity is logged and returns (I1). This is the only check that the identity is host-issued. +4. Commit point: `SelectHierarchyPath(row, path)` applies the archive-root contract and calls `CommitSelection`, which sets `_selectedRowId` to the containing row, sets `SelectedFolderPath`, posts the row render, and raises `SelectedFolderPathChanged` once. `ConfirmRow` then accepts `rowConfirmed` for that row because `_selectedRowId` matches, and `ActionOkAsync` reads the same value through `SelectedFolder` and validates it with `EfcSelectionGuard.IsValidFilingSelection`. + +Trace for the toggle path (stale completion): accept (codec shape check) -> identity validation (`TryBeginDescendantExpand` returns the node key and captured generation, or false) -> provider await (`GetImmediateSubfoldersAsync(key, CancellationToken.None)`, no `ConfigureAwait(false)`) -> post-await validation (I2: same row instance and `TryAttachDescendantChildren` accepts the generation) -> one row render. Neither the codec check nor the pre-await identity check suffices alone: the first cannot know the tree, and the second is invalidated by any collapse, rebind, or later toggle that happens during the await. + +Trace for the trash path (defect 2): `ActionDeleteAsync` -> delete gesture sets `_folderRows = WithTrashRow(_folderRows)` -> when `_formViewer` or `_router` is null, returns (guard preserved) -> otherwise awaits `BindBreadcrumbRowsAsync` with the preferred row "Trash to Delete" -> `BindRowsAsync` rebuilds rows and runs the #499 clear -> preferred-row selection sets `_selectedRowId` and `SelectedFolderPath` through the same normalization as `SelectRow` and raises `SelectedFolderPathChanged` -> `DeliverDocument` renders with `_selectedRowId` set (I5). + +Inverse constraints (must not be widened or weakened): + +- `OnHostMessageReceived` keeps catching only `BreadcrumbMessageException`. New handlers validate and log rather than throw, so no new exception type is relied on at the async-void boundary. +- `BindBreadcrumbRowsAsync` keeps its own catch-and-report boundary; the delete gesture awaits it rather than calling the router directly, so bind faults are still reported there instead of surfacing through `ButtonDeleteClickAsync`. +- The existing first-level `renderedChildActivate` path and its integer `childIndex` contract remain for key-less children. + +### Dependencies or blocked work: + +None. All seams exist: `IFolderHierarchyProvider.GetImmediateSubfoldersAsync`, `IBreadcrumbWebHost`, the in-memory router, and the node DOM harness under tests/quickfiler-breadcrumb used by #997. + +### Implementation strategy (what changes, not sequencing): + +#### Files/modules to change: + +UtilitiesCS (EFC model): +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs`: add `partial`; clear descendant state and `SelectedDescendantId` exactly where `_leafChildren` is already cleared (segment activation, `CollapseAfter`, left-arrow collapse, leaf collapse). +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` (new): keyed tree members. Proposed names: `SetLeafDescendants(IReadOnlyList)`, `TryBeginDescendantExpand(string nodeId, out FolderTreeNodeKey key, out int generation)`, `TryCollapseDescendant(string nodeId)`, `TryAttachDescendantChildren(string nodeId, int generation, IReadOnlyList)`, `GetDescendantPath(string nodeId)`, `SelectedDescendantId` with a validating setter method, and a read-only view of the root's children for the renderer. +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs`: `DescendantToggle = "descendantToggle"`, `DescendantActivate = "descendantActivate"` on `BreadcrumbMessageTypes`; optional `NodeId` on `BreadcrumbInboundMessage` (optional constructor parameter, source-compatible). +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs`: register both types in `IsKnownInboundType`; require a non-empty string `nodeId` for both, failing fast like the existing `childIndex` check. +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs`: `AppendChildren` renders recursively from the descendant tree when one exists, otherwise exactly as today. +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs`: `.child { display: flex; align-items: center; }` (merged into the existing `.child` rule); node-affordance click branch, descendant-activate click branch, and a node-affordance Enter/Space keydown branch. +- `UtilitiesCS/UtilitiesCS.csproj`: explicit `Compile Include` for the new partial. + +QuickFiler (EFC router and controller): +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` (459 lines, near the limit): two one-line delegating `case` arms in `ProcessInboundAsync`; the preferred-row parameter threaded into the existing internal bind; no other logic. +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` (new): toggle and activate handlers with the I1/I2/I3 checks. +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs`: `ExpandLeafAsync` passes the provider's keyed children to `SetLeafDescendants` (still mapping them for the unchanged `subfolderResult` message) and applies the I2 same-instance guard after its await. +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs`: preferred-row selection helper sharing `SelectRow`'s normalization (extract the selection-value computation so it is not duplicated). +- `QuickFiler/Controllers/EfcFormController.Actions.cs`: replace synchronous `ApplyDeleteGesture` with an awaited gesture; `ActionDeleteAsync` awaits it. +- `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs`: `BindBreadcrumbRowsAsync` accepts an optional preferred row and passes it to the router. +- `QuickFiler/QuickFiler.csproj`: explicit `Compile Include` for the new router partial. + +QFC page: +- `QuickFiler/Resources/FolderBreadcrumb.js`: in `makeSubfolders`, append `name` then the toggle; add a `depth` parameter (an absent value is treated as zero, so the top-level call in `render` needs no edit); set `data-depth` and `setAttribute("style", "padding-left: " + (24 + 16 * depth) + "px")` on each `.sub`; the recursive call becomes `makeSubfolders(row, child.children, selectedChildOwnsActive, depth + 1)`. The resource is a ResXFileRef to this file, so no resx change is needed. + +#### Functions/classes/CLI commands impacted: + +`BreadcrumbRow` (new partial members; reset points), `BreadcrumbMessageTypes`, `BreadcrumbInboundMessage`, `BreadcrumbMessageCodec.DeserializeInbound` / `IsKnownInboundType`, `BreadcrumbHtmlRenderer.AppendChildren`, `BreadcrumbDocumentAssets.BaseCss` / `BridgeJs`, `BreadcrumbBridgeRouter.ProcessInboundAsync` / `BindRowsAsync` / `ExpandLeafAsync` / `SelectRow` (refactor of the value computation only), `EfcFormController.ActionDeleteAsync` / `ApplyDeleteGesture` / `BindBreadcrumbRowsAsync`, and `makeSubfolders` in the QFC page. No CLI surface. + +#### Data flow and validation changes: + +- EFC expand: provider keyed children -> `SetLeafDescendants` (tree plus mapped `_leafChildren`) -> unchanged `subfolderResult` -> row render (now carrying node identities and affordances). +- EFC descendant toggle: `descendantToggle {rowId, nodeId}` -> codec shape check -> `FindRow` -> collapse (row render, no provider call) or begin expand -> provider await -> I2 check -> attach -> one row render. No `subfolderResult` is posted for descendant expansion, mirroring #997, so key-less transient children are never shown below the first level. +- EFC descendant activate: `descendantActivate {rowId, nodeId}` -> codec -> `FindRow` -> `GetDescendantPath` -> set `SelectedDescendantId` -> `SelectHierarchyPath` -> `CommitSelection`. On a rejected path (I4) the previous `SelectedDescendantId` is restored, so the highlight is unchanged. +- EFC trash: described in the trash trace above. +- Rendering of a keyed child: `
` (the first-level prefix `class="child" data-child-index=` is preserved), then a name span, then, only when the node has children, `+` or `−`. An expanded node is followed by a nested `
` placed as a sibling after the node's `.child` element (not inside it), so `closest('.child')` resolves to the clicked node and the existing `.children { margin-left: 18px; }` rule compounds per level. The selected descendant carries `aria-selected="true"` (not a second class) and only on the row that is the selected row. +- `BridgeJs` ordering: in the click listener, the node-affordance branch (posts `descendantToggle`, stops propagation, returns) precedes the `.child` branches and the leaf `.affordance` branch; the `.child[data-node-id]` branch (posts `descendantActivate`) precedes the existing key-less `.child[data-child-index]` branch, which is kept. In the keydown listener, an Enter or Space on a focused node affordance posts `descendantToggle` and returns before the existing Enter-confirms-row branch, so toggling a descendant by keyboard never confirms the row. + +#### Error handling and logging updates: + +Use the router's existing `log.Error` pattern for: unknown node identity, toggle of a leaf node, attach rejected as stale, provider fault, and cancellation during a descendant fetch. Faults and cancellation leave state unchanged and permit a later retry. No new telemetry. + +#### Rollback/feature-flag considerations (if applicable): + +No feature flag, persisted data, or migration. Reverting the diff restores prior behavior. + +### Technical specifications (interfaces/contracts): + +#### Inputs/outputs and formats: + +- New inbound JSON: `{"type":"descendantToggle","rowId":"row-0","nodeId":"<32-hex>"}` and `{"type":"descendantActivate","rowId":"row-0","nodeId":"<32-hex>"}`. Node identities are `BreadcrumbDescendantNode.Identity` values (`Guid.NewGuid().ToString("N")`), issued only by the host. +- Example filing value: with archive root `\\mailbox\Archive` and presented row `Clients\North`, activating the grandchild `Invoices` under `Q3` yields `SelectedFolderPath` = `Clients\North\Q3\Invoices`. +- Router bind: an overload or optional parameter `string? preferredRowText` on the internal `BindRowsAsync`; the public three-argument overload and the existing four-argument internal call shape remain valid for every current caller. + +#### Required configuration keys and defaults: + +None. + +#### Backward-compatibility expectations: + +- `SetLeafChildren(IEnumerable)` is unchanged; rows populated through it have no expandable descendants and render exactly as today. +- The existing seven inbound types, the `subfolderResult` message on first-level expansion, `renderedChildActivate`, and the #440 / #499 / #614 / #799 / #995 behaviors are unchanged. +- Pinned test strings preserved: `class="child" data-child-index="0"`, `type: 'renderedChildActivate'`, `e.stopPropagation();` (BreadcrumbHtmlRendererTests); `makeSubfolders(row, child.children` and the `subfolderElement.*` attribute and listener lines (FolderBreadcrumbAssetContractTests, FolderBreadcrumbRecursiveAssetIssue997Tests); no function is inserted between `makeSubfolders` and `render` (FolderBreadcrumbAssetContractTests slices bodies by adjacent function names). + +#### Performance constraints (latency/throughput/memory): + +Descendant fetches are lazy and per node; no eager subtree enumeration. Awaiting the trash bind adds no provider work (the same chain lookups already ran fire-and-forget). No numeric latency target is asserted. + +## Assumptions, Constraints, Dependencies + +- Assumptions: inbound WebView2 messages arrive on the UI thread, and EFC router continuations resume on the captured UI context (current behavior of `ExpandLeafAsync` and `FetchChainAsync`). Provider keys identify nodes uniquely across duplicate names. +- Constraints: + - Tests use MSTest, Moq, and FluentAssertions only; no temporary files; no network or external processes; strict Moq mocks of `IFolderHierarchyProvider` and `IBreadcrumbWebHost`; no real WebView2BreadcrumbHost (its static owner table is shared state under class-level parallel execution); `TaskCompletionSource` barriers for ordering, never sleeps or delays. + - New C# tests go in the sibling `.Test` projects mirroring the production folders. CLAUDE.md's unit test policies impose no `tests/` tree for C#, CLAUDE.md outranks `.claude/rules`, and every existing C# test lives in the `.Test` projects. The node suite goes under tests/quickfiler-breadcrumb, beside the #997 suites. + - All four affected projects are legacy non-SDK projects; each new `.cs` file needs an explicit `Compile Include`. + - 500-physical-line limit for production, test, and script files. Near-limit files: `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` (459) receives only delegating arms and parameter threading; EfcDataModel.cs (485) is not touched. Near-limit test files that must not be extended: EfcFormControllerTests.cs (485), EfcFormControllerTests.Part2.cs (490), BreadcrumbBridgeRouterTests.cs (462), BreadcrumbBridgeRouterQueueTests.cs (462), BreadcrumbBridgeRouterScoreJoinTests.cs (463), BreadcrumbBridgeRouterIssue439Tests.cs (455), FolderBreadcrumbAssetContractTests.cs (415). + - EFC router stays UI-thread-affine: no new locks and no `ConfigureAwait(false)` in router files (I6). Copying the QFC router's lock-plus-`ConfigureAwait(false)` model would mutate EFC row state on a pool thread concurrently with UI handlers. + - New C# files carry `#nullable enable`, consistent with their neighbors. + - `BreadcrumbDescendantNode` is consumed unchanged; any edit to it reaches the QFC router and render projection. +- External dependencies: none new. No package changes. +- CI visibility: TaskMaster CI (actionlint, build-analyzers, build-nullable, format-check, hygiene, mstest-coverage, pester) does not run the node `.mjs` suites. Every JavaScript behavior in this item (the QFC `makeSubfolders` order and depth, and the EFC `BridgeJs` branches, which are a C# string constant with no node harness) therefore has a C# text-contract test that runs under mstest-coverage. The node suite is supplementary local evidence. + +## Data / API / Config Impact + +- User-facing: EFC descendants become expandable and selectable as filing destinations; the trash row is highlighted and selected after the trash button; QFC nested rows show +/− after the name with a per-level indent. +- Internal API: two new inbound bridge types and an optional `NodeId`; new `BreadcrumbRow` members; an optional preferred-row bind parameter. All additive. +- Data/migration: none. Session state only. +- Logging: new `log.Error` lines for rejected descendant requests and fetch failures, following the existing router pattern. + +## Test Strategy + +Bugfix workflow: each defect group gets a failing regression test first, captured as a fail-before projection, then the minimal fix, then a pass-after projection. + +New test files and the scenarios each must cover (names are digit-free and binding unless the plan records a rename): + +- `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs`: `DeserializeInbound_DescendantToggle_ReturnsNodeId`, `DeserializeInbound_DescendantActivate_ReturnsNodeId`, `DeserializeInbound_DescendantMessageWithoutNodeId_Throws`, `DeserializeInbound_DescendantMessageWithEmptyNodeId_Throws`, `DeserializeInbound_DescendantMessageWithNonStringNodeId_Throws`. +- `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs`: keyed tree construction; begin expand on known, unknown, and leaf nodes; collapse; attach with matching generation, superseded generation, collapsed node, and removed node; `GetDescendantPath` for known and unknown identities; `SelectedDescendantId` validation; descendant state cleared by segment activation, `CollapseAfter`, left-arrow collapse, and leaf collapse; key-less `SetLeafChildren` yields no descendants. +- `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs`: `RenderRowFragment_KeyedChildWithChildren_RendersNodeAffordanceAfterName`, `RenderRowFragment_KeyedLeafChild_RendersNoAffordance`, `RenderRowFragment_ExpandedDescendant_NestsChildrenContainerAfterNode`, `RenderRowFragment_SelectedDescendant_MarksAriaSelected`, `RenderRowFragment_DescendantNames_AreHtmlEncoded`, `RenderRowFragment_KeylessChildren_RenderUnchanged`, `BridgeJs_NodeAffordanceBranch_PrecedesChildAndLeafBranches`, `BridgeJs_DescendantActivateBranch_PrecedesKeylessChildBranch`, `BridgeJs_NodeAffordanceEnterOrSpace_PrecedesRowConfirmation`, `BaseCss_Child_IsFlexRow`. +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` (real router, strict mocks, inbound JSON through `ProcessInboundAsync`): `DescendantToggle_SecondLevelNode_FetchesWithNodeKeyAndRendersChildren` (fail-before: codec rejects the type), `DescendantToggle_ExpandedNode_CollapsesWithoutProviderCall`, `DescendantToggle_DoesNotChangeSelection`, `DescendantToggle_PostsNoSubfolderResult`, `DescendantActivate_Grandchild_CommitsArchiveRelativeStem`, `DescendantActivate_ThenRowConfirmed_RaisesConfirmationOnce`, `DescendantActivate_OutsideArchiveRoot_LeavesSelectionAndHighlightUnchanged`, `DescendantRequest_UnknownNode_IsRejectedWithoutStateChange`, `DescendantToggle_LeafNode_IsRejectedWithoutProviderCall`, `DescendantRequest_NodeFromAnotherRow_IsRejected`, `ExpandLeaf_KeyedChildren_RenderNodeAffordances`. +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs`: `DescendantFetch_CompletesAfterCollapse_AttachesNothing`, `DescendantFetch_CompletesAfterRebind_PostsNothingForReplacedRow`, `DescendantFetch_ReverseCompletion_OnlyLatestGenerationApplies`, `DescendantFetch_ProviderFault_LeavesStateUnchangedAndAllowsRetry`, `DescendantFetch_Canceled_LeavesStateUnchanged`, `ExpandLeaf_CompletesAfterRebind_PostsNothingForReplacedRow`, `ExpandLeaf_CurrentRow_StillPostsSubfolderResultThenRender`. +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs`: `BindRows_PreferredTrashRow_SelectsTrashInDeliveredDocument`, `BindRows_PreferredTrashRow_LastSelectionNotificationCarriesTrash`, `BindRows_PreferredTrashRow_PostsNoFollowUpRender`, `BindRows_PreferredRowAbsent_SelectsNothing`, `BindRows_NoPreferredRow_KeepsExistingClearBehavior`, `BindRows_PreferredTrashRowBeforeCoreInit_PendingDocumentCarriesSelection`. +- `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` (new partial of `EfcFormControllerTests`; the two existing files are near the limit): `ActionDeleteAsync_WithRouter_SelectsTrashEntry` (fail-before: `SelectedFolder` is null), `ActionDeleteAsync_WithRouter_DeliveredDocumentMarksTrashSelected`, `ActionDeleteAsync_AwaitedTwice_KeepsTrashSelectedAndSingleTrashRow`, `ActionDeleteAsync_BindFault_IsReportedAtBindBoundary`. +- `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` (CI-visible contract over `FolderBreadcrumbDocument.Html`): `MakeSubfolders_AppendsNameBeforeToggle` (fail-before), `MakeSubfolders_RecursiveCallPassesDepthPlusOne` (fail-before), `MakeSubfolders_SetsDataDepthAndPerDepthPadding`, `MakeSubfolders_PinnedRecursiveCallPrefixPreserved`. +- `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs` (node DOM harness over the real FolderBreadcrumb.js; not run by CI): for every `.sub` with a toggle, the toggle's child index is greater than the name's; `.sub` elements at levels 0, 1, 2 carry `data-depth` 0, 1, 2 and padding-left 24px, 40px, 56px. Run together with the #997 suites in normal, reversed, and shuffled order, as #997 recorded. + +Existing tests to keep green with no edits: BreadcrumbHtmlRendererTests, BreadcrumbMessageCodecTests, BreadcrumbRowStateTests, BreadcrumbBridgeRouterTests (and .Selection), BreadcrumbBridgeRouterQueueTests (and .Part2), BreadcrumbBridgeRouterScoreJoinTests, BreadcrumbBridgeRouterIssue439Tests (and .Activation), BreadcrumbBridgeRouterIssue614Tests, BreadcrumbBridgeRouterIssue637Tests, BreadcrumbBridgeRouterIssue792Tests, BreadcrumbBridgeRouterIssue995Tests, EfcFormControllerTests (including `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows`, which relies on the null-router guard), EfcFormControllerIssue995Tests, EfcHomeControllerExecuteMovesTests, FolderBreadcrumbAssetContractTests, FolderBreadcrumbRecursiveAssetIssue997Tests, and the issue-997 node suites. Test disposition: no test references `ApplyDeleteGesture` except in a comment, and no test reaches router or row members by reflection name that this fix renames; the plan re-runs that grep (`GetField(` / `GetMethod(` / `GetProperty(` plus each renamed member) before marking any file unchanged. + +Edge cases and negative scenarios: unknown row; unknown, leaf, and cross-row node identities; malformed `nodeId`; collapse during fetch; rebind during fetch; reversed completions; provider fault; cancellation; out-of-root and archive-root activation; preferred row absent; host core not yet initialized; delete gesture with a null router. + +Coverage: new classes, partials, and methods target at least 90% line coverage; changed lines must not lose coverage. Repository floors are 80% line and 75% branch (CLAUDE.md § UT2, testable denominator). No baseline exists yet in this folder; the plan captures it, and if the raw repository figure is already below a floor the blocking condition is change-scoped and the repository figure is recorded and shown not lowered. The JS file is not measured by the C# coverage tool; the node `--experimental-test-coverage` figure for FolderBreadcrumb.js is recorded as #997 did. No coverage exclusions are added. + +Toolchain (CLAUDE.md order, one clean pass, restart from step 1 on any failure or file change): +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. The `test: MSTest with Coverage (Koverage)` VS Code task, or Invoke-MSTestWithCoverage.ps1 under scripts/vscode directly. + +Evidence (Markdown projections only, each with Timestamp, Command, EXIT_CODE, and an output summary; no host paths; no raw TRX, Cobertura, or .coverage file is added to the repository): +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-toolchain.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/baseline/baseline-coverage.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-descendants-fail-before.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-descendants-pass-after.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-trash-fail-before.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/efc-trash-pass-after.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/qfc-nesting-fail-before.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/qfc-nesting-pass-after.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/regression-testing/node-suites-run.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/final-toolchain.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/qa-gates/coverage-comparison.md` +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/evidence/other/manual-outlook-check.md` + +Manual validation (non-blocking human check): open EFC on a mailbox with a three-level tree; expand a suggested folder, expand a child, select a grandchild, press OK, and confirm the filing destination; press the trash button and confirm the trash row is highlighted and OK targets it; open QFC, expand a suggested folder and a child with subfolders, and confirm the +/− follows the name and each level is indented one step further. This requires a live Outlook process, a mailbox, and the WebView2 runtime, so it cannot be automated here. It is recorded in manual-outlook-check.md as a pending human-interaction step with these instructions; it must not be claimed as executed by an agent, and it does not block completion. + +## Acceptance Criteria + +Criteria are labelled ACn and grouped by defect. Each is one checkbox. + +### Group A: EFC recursive subfolder expansion and selection (defect one) + +- [x] AC1 Codec contract: `BreadcrumbMessageCodec.DeserializeInbound` accepts `descendantToggle` and `descendantActivate` carrying a `rowId` and a non-empty string `nodeId`, exposing the identity as `BreadcrumbInboundMessage.NodeId`; a missing, empty, or non-string `nodeId` on either type throws `BreadcrumbMessageException`; every previously accepted inbound type deserializes exactly as before. Verified by `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` and the unchanged BreadcrumbMessageCodecTests. +- [x] AC2 Row state: `BreadcrumbRow` holds a keyed descendant tree built from keyed first-level children, reusing BreadcrumbDescendantNode with that file absent from the diff; expansion begins only for a known node that has children, attach is rejected when the node was removed, collapsed, or its request generation moved, unknown identities resolve to no path, and the tree and the selected descendant are cleared at every point where the first-level children are already cleared; rows populated through the key-less `SetLeafChildren` have no expandable descendants. Verified by `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` and the unchanged BreadcrumbRowStateTests. +- [x] AC3 First-level expansion keeps provider keys: `ExpandLeafAsync` stores the provider's keyed children on the row, so a first-level child that has subfolders renders a node affordance, while the current-row sequence of a `subfolderResult` message followed by a row render is unchanged. +- [x] AC4 Descendant toggle: a `descendantToggle` for a second-level node calls `GetImmediateSubfoldersAsync` exactly once with that node's key and posts one row render whose markup nests the returned children below that node; a second toggle collapses the node with no provider call; no `subfolderResult` is posted for descendant expansion; and a toggle never changes `SelectedFolderPath` nor raises `SelectedFolderPathChanged`. +- [x] AC5 Descendant activation is the filing destination: with an archive root bound, a `descendantActivate` for a grandchild sets `SelectedFolderPath` to its archive-relative stem through `SelectHierarchyPath` (for example the stem Clients\North\Q3\Invoices), raises `SelectedFolderPathChanged` exactly once, makes the containing row the selected row with the descendant marked selected in the posted render, satisfies `EfcSelectionGuard.IsValidFilingSelection`, and a following `rowConfirmed` for that row raises `ConfirmationRequested` exactly once; an out-of-root or archive-root target leaves the prior selection, the event stream, and the rendered highlight unchanged. +- [x] AC6 Identity validation: a descendant message naming an unknown row, an unknown node, a node from another row, or (for a toggle) a leaf node is logged and rejected with no provider call, no state change, no posted message, and no selection event. +- [x] AC7 Descendant stale-completion guard: a descendant fetch whose provider task completes after the node was collapsed, after a rebind replaced the row, or after a later toggle superseded its generation attaches nothing and posts nothing for that row id; a provider fault or cancellation is logged, leaves state unchanged, and a later toggle of the same node succeeds. Verified with `TaskCompletionSource` barriers in `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs`. +- [x] AC8 First-level stale-completion guard: when the provider task of an existing first-level `ExpandLeafAsync` completes after a rebind replaced the row, the replaced row is not mutated and neither a `subfolderResult` nor a render is posted for that row id; a completion for the current row behaves exactly as before. +- [x] AC9 Recursive rendering: `BreadcrumbHtmlRenderer` renders each keyed child with its node identity and depth, a name element, and, only when the node has children, a focusable node affordance with button role and expanded state placed after the name; leaf nodes carry no affordance; an expanded node is followed by a nested children container that is a sibling of the node element rather than inside it; display names and paths are HTML-encoded; the selected descendant is marked with an aria-selected attribute rather than a new class; the first-level child markup prefix pinned by BreadcrumbHtmlRendererTests is unchanged; and key-less children render exactly as before. Verified by `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs`. +- [x] AC10 EFC bridge script contract: in `BreadcrumbDocumentAssets.BridgeJs`, the node-affordance click branch posts `descendantToggle` with row and node identity, stops propagation, and precedes both the child branches and the leaf affordance branch; a child branch keyed by node identity posts `descendantActivate` and precedes the retained key-less `renderedChildActivate` branch; Enter or Space on a focused node affordance posts `descendantToggle` and returns before the existing Enter-confirms-row branch; and the child rule in `BreadcrumbDocumentAssets.BaseCss` is a flex row so the affordance follows the name. Because CI runs no browser engine for this C# string constant, these orderings are asserted by C# text-contract tests in the Group A renderer test file. + +### Group B: EFC trash button selects the trash entry (defect two) + +- [x] AC11 Router preselection in the same bind: `BreadcrumbBridgeRouter.BindRowsAsync` accepts an optional preferred row; when the preferred text is "Trash to Delete" and the trash row is bound, `SelectedFolderPath` equals "Trash to Delete" before the document is delivered, the single navigated document (or the pending document when the host core is not yet initialized) marks the trash row as the selected rowwrap, exactly one `SelectedFolderPathChanged` notification of the bind carries the trash text and it is the last notification of the bind, and no outbound render message is posted to establish that selection. The preferred-row value computation is shared with `SelectRow`, not duplicated. Verified by `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs`. +- [x] AC12 Router compatibility: a bind without a preferred row, or with a preferred row that is not present, behaves exactly as before, including the existing clear of a stale selection, and every existing caller of the public and internal bind compiles and passes unchanged. +- [x] AC13 Controller end to end: after `await ActionDeleteAsync()` on a controller wired to a real router over strict mocks, `SelectedFolder` equals "Trash to Delete" and the last navigated document marks the trash row selected; a second awaited invocation keeps the trash row selected and `_folderRows` holds exactly one trash row; the bind is awaited inside `BindBreadcrumbRowsAsync` so a bind fault is still reported at that boundary; the null-router and null-viewer guard is preserved and `ActionDeleteAsync_AwaitedTwice_LeavesExactlyOneTrashRowInFolderRows` passes unchanged. Verified by `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs`. + +### Group C: QFC nested row affordance and indentation (defect three) + +- [x] AC14 Affordance after the name at every depth: in `makeSubfolders` in `QuickFiler/Resources/FolderBreadcrumb.js`, the name element is appended before the toggle element, so on every descendant row at every depth the +/− immediately follows the folder name as on top-level rows (not flush right); the toggle keeps its click and Enter/Space handlers and the descendant-toggle message unchanged. Asserted CI-visibly by `MakeSubfolders_AppendsNameBeforeToggle` in `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs`. +- [x] AC15 One indent step per level: `makeSubfolders` takes a depth that is zero when absent, the recursive call passes depth plus one, and each descendant row carries a data-depth attribute equal to its depth and an inline left padding of twenty-four pixels plus sixteen pixels per depth level, so each nested level sits one sixteen-pixel step further in than its visible parent while the selected-row highlight stays full width. Asserted CI-visibly by the depth tests in the Group C nesting contract file; the `render` function, `makeRow`, the message listener, FolderBreadcrumb.html, and every pinned `makeSubfolders` and `subfolderElement` string in FolderBreadcrumbAssetContractTests and FolderBreadcrumbRecursiveAssetIssue997Tests are unchanged, and no function is inserted between `makeSubfolders` and `render`. +- [x] AC16 Node behavioral suite: the new node nesting suite named in the Write Set asserts, against the real page script, that the toggle follows the name on every descendant row and that rows at depths zero, one, and two carry increasing depth attributes and left padding; it passes together with the predecessor recursive-selection node suites in normal, reversed, and shuffled order, recorded in the regression-testing projection node-suites-run. Because CI does not run node suites, this criterion is satisfied by that local record and does not replace AC14 or AC15. + +### Group D: Cross-cutting scope, quality, and evidence + +- [x] AC17 Fail-before and pass-after evidence: for each defect group, the named fail-before tests fail on the unfixed tree and pass after the fix, recorded in the regression-testing projections efc-descendants-fail-before, efc-descendants-pass-after, efc-trash-fail-before, efc-trash-pass-after, qfc-nesting-fail-before, and qfc-nesting-pass-after (full paths in Test Strategy). +- [x] AC18 Scope fences: the diff modifies no file in the explicitly excluded list in Scope and Non-Goals (the QFC router, selection session, coordinator, render projection and wiring files, BreadcrumbDescendantNode, FolderBreadcrumb.html, the resource table, and the EFC data model); EFC does not reference the QFC selector document or the QFC router; and no new arrow-key handling is added to the EFC router or bridge script. +- [x] AC19 Thread affinity: no file matching `QuickFiler/Controllers/BreadcrumbBridgeRouter*.cs` contains `ConfigureAwait(false)` or a lock statement after the change, the new descendant handler awaits the provider without `ConfigureAwait`, and the delete gesture path in `QuickFiler/Controllers/EfcFormController.Actions.cs` adds no `ConfigureAwait(false)`. +- [x] AC20 File size: each new or modified C# file in the Write Set and `QuickFiler/Resources/FolderBreadcrumb.js` is at most five hundred physical lines after the change (project files and Markdown are exempt under the repository rule), `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` gains only delegating case arms and preferred-row parameter threading, and none of the near-limit test files listed in Constraints is extended. +- [x] AC21 Test policy and registration: every new C# test uses MSTest, Moq, and FluentAssertions, follows Arrange-Act-Assert, creates no temporary or other files, uses strict mocks and no real WebView2 host, and uses no sleep or delay; each new C# source and test file has an explicit compile entry in its legacy project file, and every new test name appears in the executed test list of the final MSTest run. +- [x] AC22 Existing tests: every existing test listed in Test Strategy as kept green passes with no edit to its assertions or pinned strings. +- [x] AC23 Coverage: each new class, partial, and method in the Write Set reaches at least ninety percent line coverage, changed lines lose no coverage, no coverage exclusion is added other than the five member-level `[ExcludeFromCodeCoverage]` attributes on `ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `RefreshSuggestionsAsync` and `ConfigureBreadcrumbControl` ratified by the maintainer under CLAUDE.md UT2 (remediation-inputs.2026-10-22T10-39.md, B-3 Maintainer decision); no `coverage.config` entry and no class-level exclusion is added, and the repository line and branch figures are recorded against the eighty percent line and seventy-five percent branch floors and shown not lowered by the change, in the qa-gates projection coverage-comparison against the baseline projection baseline-coverage. +- [x] AC24 Toolchain: one complete pass of format, analyzers rebuild, warnings-as-errors rebuild, and MSTest with coverage, in the CLAUDE.md order and with the CLAUDE.md commands, succeeds with no failure or file change, recorded in the qa-gates projection final-toolchain. +- [x] AC25 Evidence form: all evidence is committed as Markdown projections under the feature folder's evidence directories, and the diff adds no raw test-result, Cobertura, or binary coverage file. +- [x] AC26 Manual Outlook check recorded as non-blocking: the other-evidence projection manual-outlook-check records the manual EFC and QFC verification steps from Test Strategy as a pending human-interaction step, not as executed by an agent; this criterion is satisfied by that record, and the human check itself does not block completion. + +## Write Set + +Production: +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.cs` +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbRow.Descendants.cs` (new) +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessages.cs` +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbMessageCodec.cs` +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbHtmlRenderer.cs` +- `UtilitiesCS/OutlookObjects/Folder/BreadcrumbDocumentAssets.cs` +- `UtilitiesCS/UtilitiesCS.csproj` +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.cs` +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Descendants.cs` (new) +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Arrows.cs` +- `QuickFiler/Controllers/BreadcrumbBridgeRouter.Selection.cs` +- `QuickFiler/Controllers/EfcFormController.Actions.cs` +- `QuickFiler/Controllers/EfcFormController.Breadcrumb.cs` +- `QuickFiler/Resources/FolderBreadcrumb.js` +- `QuickFiler/QuickFiler.csproj` + +Tests: +- `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbMessageCodecIssue1002Tests.cs` (new) +- `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbRowDescendantsIssue1002Tests.cs` (new) +- `UtilitiesCS.Test/OutlookObjects/Folder/BreadcrumbHtmlRendererIssue1002Tests.cs` (new) +- `UtilitiesCS.Test/UtilitiesCS.Test.csproj` +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterIssue1002Tests.cs` (new) +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterStaleCompletionIssue1002Tests.cs` (new) +- `QuickFiler.Test/Controllers/BreadcrumbBridgeRouterTrashIssue1002Tests.cs` (new) +- `QuickFiler.Test/Controllers/EfcFormControllerTests.Issue1002.cs` (new) +- `QuickFiler.Test/Viewers/FolderBreadcrumbNestingIssue1002Tests.cs` (new) +- `QuickFiler.Test/QuickFiler.Test.csproj` +- `tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs` (new; local evidence only, not run by CI) + +Feature documents and evidence: +- `docs/features/active/2026-10-22-efc-qfc-folder-tree-selection-and-nesting-1002/**` + +## Risks & Mitigations + +- Near-limit router file (459 lines): only delegating arms and parameter threading go in it; logic goes in the new partials. Verified by a whole-file line count after edits (AC20). +- Stale completions over positional row ids (`row-` is reused across binds): the same-instance and generation checks of I2, applied to both fetch paths (AC7, AC8). +- Thread affinity: copying the QFC router's concurrency model would race UI handlers; I6 and AC19 forbid it. +- Click and keydown routing in `BridgeJs`: the node affordance sits inside the `.child` element and the document-level Enter handler confirms the row, so branch order is load-bearing; AC10 pins it by text contract because no browser engine runs in CI. +- Shared type reuse: `BreadcrumbDescendantNode` is used unchanged (AC2, AC18) because QFC depends on it. +- QFC page shared by the collapsed and popup surfaces: the edit is confined to `makeSubfolders`, and the pinned strings constrain it (AC15, AC22). +- JS behavior has no CI signal: paired C# text contracts (AC10, AC14, AC15) plus the local node suite (AC16). +- Delete-gesture exception path: awaiting the bind inside `BindBreadcrumbRowsAsync` keeps faults reported at the existing boundary (AC13). +- Rollback: revert the diff; no persisted data or configuration is affected. + +## Rollout & Follow-up + +- Release: regression-first implementation, ordered baseline and final QA, acceptance tracking, feature review, and final-head CI through the orchestrator; shipped with the normal TaskMaster release. No migration. +- Follow-up: the maintainer performs the manual Outlook check recorded in manual-outlook-check.md. No other follow-up is planned. +- Links: Issue https://github.com/drmoisan/TaskMaster/issues/1002; issue.md and the research record in this folder; design precedent docs/features/archive/2026-10-17-quickfiler-recursive-folder-selection-997/spec.md (Issue #997, PR #998). + +## Planner Amendments + +- 2026-10-22: AC23 amended. The clause "no coverage exclusion is added" was replaced with the text naming the five member-level `[ExcludeFromCodeCoverage]` attributes (`ActionOkAsync`, `ActionCancelAsync`, `CreateFolderAsync`, `RefreshSuggestionsAsync`, `ConfigureBreadcrumbControl`) and stating that no `coverage.config` entry and no class-level exclusion is added; the criterion remains checked. Authority: maintainer decision recorded 2026-10-22T10:50Z in `remediation-inputs.2026-10-22T10-39.md`, B-3 Maintainer decision. Reason: cycle-0 finding B-3; the five attributes exist on the branch and the criterion was false as written. diff --git a/docs/features/potential/promoted/2026-10-22-ci-mstest-testhost-crash-in-folder-tree-service-tests.md b/docs/features/potential/promoted/2026-10-22-ci-mstest-testhost-crash-in-folder-tree-service-tests.md new file mode 100644 index 000000000..1f38e7058 --- /dev/null +++ b/docs/features/potential/promoted/2026-10-22-ci-mstest-testhost-crash-in-folder-tree-service-tests.md @@ -0,0 +1,64 @@ +# ci-mstest-testhost-crash-in-folder-tree-service-tests (Issue #1004) + +- Date captured: 2026-10-22 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/ci-mstest-testhost-crash-in-folder-tree-service-tests/ (Issue #1004) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #1004 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/1004 +- Last Updated: 2026-10-22 +## Summary + +The CI `mstest-coverage` job intermittently aborts with "Test host process crashed" while the UtilitiesCS.Test `OutlookFolderTreeService` concurrency, disposal and invalidation tests are running. No assertion fails; the test host process dies, so the run reports an unknown total and fails the job. + +## Environment + +- OS/version: GitHub Actions `windows` runner, Visual Studio 18 test platform (`vstest.console.exe`) +- Python version: not applicable (C# .NET Framework 4.8 MSTest) +- Command/flags used: `scripts/vscode/Invoke-MSTestWithCoverage.ps1` via the CI `mstest-coverage` job +- Data source or fixture: UtilitiesCS.Test `OutlookFolderTreeServiceConcurrencyTests`, `OutlookFolderTreeServiceDisposalTests`, `OutlookFolderTreeServiceInvalidationTests` + +## Steps to Reproduce + +1. Run the CI workflow on a pull request (observed on PR #1003, run 39794073798, attempt 1, head e94c261302dcc2298b926f8682c89ad7448352f6). +2. Observe the `mstest-coverage` job: 4622 tests pass, then "The active test run was aborted. Reason: Test host process crashed". +3. Re-run the failed job on the same commit: attempt 2 passed all checks, so the crash is intermittent. + +## Expected Behavior + +The MSTest run completes deterministically; no test leaves an unhandled exception on a background or dispatcher thread that can terminate the test host. + +## Actual Behavior + +Intermittent test host crash. The last tests reported before the crash were `GetSnapshotAsync_WorkerOriginatedColdBuild_UsesCapturedStaDispatcher`, `Dispose_DuringRefresh_LeavesDisposedWithoutPublicationOrNotification`, `FolderChanged_DuringInFlightBuild_SchedulesOneFollowUpRefresh`, `NotificationRefreshAndDispose_RunOnTheCapturedDispatcher` and `NotificationRefresh_RunsOnCapturedDispatcher`. + +## Logs / Screenshots + +- [x] Attached minimal logs or screenshot +- Snippet: `The active test run was aborted. Reason: Test host process crashed : Process path: ...\TestPlatform\Extensions\..\testhost.exe` / `Test Run Aborted. Total tests: Unknown Passed: 4622` + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +## Suspected Cause / Notes + +- An unhandled exception on a dedicated STA dispatcher thread or thread-pool continuation (for example a dispatcher callback or refresh continuation that outlives its test and throws after disposal) terminates the whole test host under .NET Framework. +- The uploaded CI artifact holds only the Cobertura document, not the trx or a blame/crash dump, so the crashing test is inferred from log order. Adding `--Blame` (crash collection) to the CI vstest invocation would name the test that was running. +- Issue #1002 / PR #1003 did not modify `OutlookFolderTreeService` or these test classes; its UtilitiesCS changes are synchronous types only. + +## Proposed Fix / Validation Ideas + +- [ ] Unit coverage areas: guard dispatcher and continuation callbacks in `OutlookFolderTreeService` against post-dispose execution; make the concurrency tests join or drain their dispatcher threads before returning +- [ ] Integration scenario to retest: repeated CI runs of UtilitiesCS.Test in the CI shape, with blame crash collection enabled +- [ ] Manual verification notes: none + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/potential/promoted/2026-10-22-efc-qfc-folder-tree-selection-and-nesting.md b/docs/features/potential/promoted/2026-10-22-efc-qfc-folder-tree-selection-and-nesting.md new file mode 100644 index 000000000..139bb6611 --- /dev/null +++ b/docs/features/potential/promoted/2026-10-22-efc-qfc-folder-tree-selection-and-nesting.md @@ -0,0 +1,68 @@ +# efc-qfc-folder-tree-selection-and-nesting (Issue #1002) + +- Date captured: 2026-10-22 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/efc-qfc-folder-tree-selection-and-nesting/ (Issue #1002) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #1002 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/1002 +- Last Updated: 2026-10-22 +## Summary + +Three defects in the QuickFiler folder-selection surfaces. (1) The EFC viewer does not support selecting a subfolder of a listed folder the way the QFC viewer now does after issue #997 (PR #998). (2) Pressing the EFC trash button creates a trash entry but does not select it. (3) In the QFC viewer, a subfolder that has its own subfolders shows its expand affordance (+/−) on the left instead of the right, and deeper levels are not indented beneath their visible parent. + +## Environment + +- OS/version: Windows 11, Outlook desktop (VSTO add-in), WebView2 breadcrumb surface +- Python version: not applicable (C# .NET Framework 4.8, JavaScript breadcrumb asset) +- Command/flags used: interactive use of the EFC and QFC viewers +- Data source or fixture: any mailbox whose folder tree has at least three nested levels + +## Steps to Reproduce + +1. Open the EFC viewer, open the folder suggestions, and try to drill into and select a subfolder of a suggested folder. +2. In the EFC viewer, press the trash button. +3. Open the QFC viewer, expand a suggested folder, then look at a child subfolder that itself has subfolders and expand it. + +## Expected Behavior + +1. EFC offers the same recursive subfolder expansion and selection that QFC gained in #997, and the selected subfolder becomes the filing destination. +2. After the trash button adds the trash entry, that entry is the selected entry. +3. In QFC, the expand affordance of every expandable subfolder row sits on the right of the row, matching the top-level rows, and each nested level is indented one step further than its visible parent. + +## Actual Behavior + +1. EFC does not offer recursive subfolder selection. +2. The trash entry is created but the selection stays where it was. +3. The expand affordance of a nested subfolder renders on the left, and nested levels share one indent. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: reported by the maintainer on 2026-10-22; no log excerpt. + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +## Suspected Cause / Notes + +- The #997 fix (merge 59b888d38) added descendant support in `QuickFiler/Resources/FolderBreadcrumb.js`, `UtilitiesCS/OutlookObjects/Folder/FolderBreadcrumbBridgeRouter.Descendants.cs`, `BreadcrumbSelectionSession.Descendants.cs`, `QuickFiler/Viewers/BreadcrumbBridgeCoordinator.Descendants.cs`, and wiring in `QfcItemController.BreadcrumbWiring.cs` and `ItemViewer.Breadcrumb.cs`. The EFC path (`EfcViewer`, `EfcFormController`) is likely not wired to the descendant messages. +- Trash handling lives in `EfcFormController.Actions.cs` and `EfcDataModel.cs`. +- The subfolder toggle is appended after the label in `FolderBreadcrumb.js` (around the `subfolderElement.appendChild(toggle)` call); the left placement and flat indent are likely CSS or DOM-order issues in the descendant row rendering. + +## Proposed Fix / Validation Ideas + +- [ ] Unit coverage areas: EFC descendant wiring, trash selection, descendant row rendering order and depth indentation +- [ ] Integration scenario to retest: three-level folder tree in EFC and QFC +- [ ] Manual verification notes: affordance on the right at every level; indent increases per level + +## Next Step + +- [ ] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs b/tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs new file mode 100644 index 000000000..a13e2e2a3 --- /dev/null +++ b/tests/quickfiler-breadcrumb/issue-1002-nesting.test.mjs @@ -0,0 +1,47 @@ +// Issue 1002: in the QFC selector, every nested subfolder row shows its +/- after the name, +// and each nested level is indented one 16px step further than its visible parent. +import test from "node:test"; +import assert from "node:assert/strict"; +import { source } from "./issue-997-source-loader.mjs"; +import { createBrowser, suggestionRow, selectorView } from "./issue-997-browser-harness.mjs"; + +function node(identity, children = []) { + return { identity, displayName: identity, folderPath: `Projects\\Apollo\\${identity}`, + hasChildren: children.length > 0, expanded: children.length > 0, selected: false, children }; +} + +// Three expanded levels: Alpha (depth 0) > Beta (depth 1) > Gamma (depth 2), plus a sibling leaf. +function show() { + const browser = createBrowser(source); + const tree = [node("Alpha", [node("Beta", [node("Gamma")])]), node("Omega")]; + browser.receive({ type: "render", rows: [suggestionRow({ leafExpanded: true, subfolders: tree })] }); + browser.receive(selectorView()); + return browser; +} + +function sub(browser, identity) { + return browser.list.querySelectorAll(".sub").find((element) => element.id === `folder-option-0-node-${identity}`); +} + +test("nested toggle follows the name at every depth", () => { + const browser = show(); + const withToggle = browser.list.querySelectorAll(".sub").filter((element) => element.querySelector(".aff")); + assert.equal(withToggle.length, 2, "Alpha and Beta are expandable"); + for (const element of withToggle) { + const name = element.children.indexOf(element.querySelector(".seg")); + const toggle = element.children.indexOf(element.querySelector(".aff")); + assert.ok(name >= 0, "the name is a direct child of the row"); + assert.ok(toggle > name, `the toggle of ${element.id} follows its name`); + } +}); + +test("nested rows carry data-depth and per-depth padding", () => { + const browser = show(); + const expected = [["Alpha", "0", 24], ["Beta", "1", 40], ["Gamma", "2", 56], ["Omega", "0", 24]]; + for (const [identity, depth, padding] of expected) { + const element = sub(browser, identity); + assert.ok(element, `${identity} is rendered`); + assert.equal(element.getAttribute("data-depth"), depth); + assert.equal(element.getAttribute("style"), `padding-left: ${padding}px`); + } +});