Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/agent-memory/feature-review/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@
- [epic fan-in artifact path + hook regex traps](project_epic_fanin_artifact_path_and_hook_regex.md) — hook regex demands docs/features/active/; UNVERIFIED is a narrowing word and "Pester" alone satisfies both label+coverage tests
- [build-ci-coverage-gate-fidelity epic outcome](project_build-ci-coverage-gate-fidelity-epic-outcome.md) — 80-vs-85 contradiction NOT removed; runner gate 80 vs hook 85; C# 85.55%, PS branch nonexistent
- [PowerShell coverage is nondeterministic](project_powershell-coverage-nondeterministic-vsbuild-tests.md) — Invoke-VSBuild.Tests.ps1 runs vswhere + Sync-PackageReferences for real; measure PS coverage in-session, never quote a stored figure
- [449-review-residuals](project_449-review-residuals.md) — PASS/0 blocking; untracked #584 promotion doc owed a non-child route; unused usings in base test file; AC-supersession-via-plan-provision pattern validated
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: 449-review-residuals
description: '#449 QfcExplorerController review: PASS/0 blocking; residuals: untracked #584 promotion doc needs non-child commit route; unused usings in base test file; AC-12/AC-16 supersession pattern validated'
metadata:
type: project
---

#449 (epic quickfiler-suite-determinism-foundation, wave 0) reviewed 2026-08-22: PASS, 0 blocking, all 16 spec ACs verified.

**Why:** three residuals survive the merge and matter at epic close or later reviews.

**How to apply:**
- `docs/features/potential/promoted/2026-08-22-uithread-dispatcher-null-race-progresstrackerasync.md` (Issue #584, the ProgressTrackerAsync STA flake) sits UNTRACKED in the 449 worktree — the epic forbids children committing under `docs/features/potential/**`, so verify at epic close that it reached the repo via a non-child route or that issue #584's body is accepted as the durable record.
- `QuickFiler.Test/Controllers/QfcExplorerControllerTests.cs:1-2` carries two unused usings (`System.Collections`, `System.Collections.Generic`) stranded by the 500-line-cap partial-class split; fix opportunistically on next touch.
- Validated pattern (reusable): when a plan's own split provision ([P6-Tn]) fires and supersedes an AC's literal figure ("exactly one appended line" -> two; 485 -> 486), the AC stays PASS if the supersession is recorded in a dedicated evidence artifact and carried into the check-off notes — do not raise the numeric divergence alone as a defect (cf. [[441-review-residuals-and-494-handoff]]).
- Coverage adjudication precedent: with `artifacts/csharp/coverage.xml` deliberately absent and no pr_context summary, verifying from the executor's raw `coverage/*.cobertura.xml` on disk (recompute root/package/per-file figures with a scratch parser) satisfies the evidence-verification model; a class suppressed by a class-level `[ExcludeFromCodeCoverage]` at baseline is "absent from the report", not 0% — a per-file grep for matching `<class>` elements (0 at baseline, 4 after) proves it.
2 changes: 2 additions & 0 deletions .claude/agent-memory/orchestrator/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@
- [#457 coverage moved UP, and the kickoff figure was wrong](project_457_coverage_moved_up_not_down.md) — the denominator fix raised the rate 85.3514%→85.5355%; kickoff's 85.0317% matched no measurement, so #494 must re-measure
- [atomic-planner has no MCP validator tool](atomic-planner-lacks-mcp-validator-tool.md) — it cannot run the mandatory plan gate; run it yourself and tell the planner not to fabricate a result
- [C# agent worktree needs three bootstrap steps](csharp-agent-worktree-needs-three-bootstrap-steps.md) — no `.dotnet-sdk`, no `packages/`, and a clean restore still hits `error CS0006` on skewed analyzer versions; green CI is cache-explained, not tolerance
- [potential_to_issue keeps ONLY the Summary section](potential-to-issue-keeps-only-summary-section.md) — every other section becomes "(not provided in potential file)"; verify the issue body, post the rest as a comment
- [Epic kickoff facts need independent measurement](epic-kickoff-facts-need-independent-measurement.md) — a kickoff predicted a 500-line cap finding from a conflated file; the named file was 323 lines, not 1,065
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
name: epic-kickoff-facts-need-independent-measurement
description: An epic kickoff can assert a measured fact that is false by file conflation and instruct the child to act on it; measure the named file yourself before accepting a predicted Blocking finding
metadata:
type: feedback
---

Measure every quantitative fact an epic kickoff states about a file before acting on it, especially
when the kickoff uses that fact to PREDICT a finding and prescribe a response.

**Why:** On the #449 child of `quickfiler-suite-determinism-foundation`, the kickoff stated that
`QuickFiler/Controllers/QfcExplorerController.cs` "is 1,065 lines today", that `feature-review` would
therefore raise the 500-line-cap violation, and that the child should record it as out-of-scope and
"promote the split as its own issue". Measured on the branch, the file was **323** lines — under the
cap. The 1,065-line file is `QuickFiler/Legacy/QuickFileController.cs`, an uncompiled file the change
never touches. The kickoff had conflated two similarly named controllers in the same feature area.

Acting on the stated figure would have produced a fabricated cap-violation finding, a spurious
promoted issue, and possibly an out-of-scope partial-class split — all defended by citing the
kickoff. The committed plan's own tasks already carried the correct attribution, so the plan and the
kickoff disagreed and the plan was right.

**How to apply:**
- Treat a kickoff's `file:line` and line-count claims exactly like a promoted potential's citations:
as claims to re-derive, not inputs. `wc -l` is one command.
- A kickoff that predicts a specific finding is the highest-risk case, because the prediction primes
both the orchestrator and the reviewer to confirm it. Brief the reviewer with the MEASURED value
and tell it explicitly not to raise a finding on a file that is not in the diff.
- When the kickoff and the committed plan disagree on a fact, prefer the plan — it was written
against the tree — then verify both against the file.
- Record the correction in the checkpoint (a `plan_drift_notes`-style key) so the epic parent can see
the kickoff defect rather than silently inheriting it.

Related: [[feedback_plan_phase0_paths_are_stale_in_epic_children]] (the same kickoff also named a
stale preparation worktree as WORKTREE), [[feedback_verify_subagent_capability_claims]],
[[feedback_verify_child_preflight_clearance]].
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
name: potential-to-issue-keeps-only-summary-section
description: potential_to_issue copies ONLY the "## Summary" section into the GitHub issue body and stubs every other bug-template section with "(not provided in potential file)" — all other analysis is silently dropped
metadata:
type: project
---

`mcp__drm-copilot__potential_to_issue` does NOT copy the promoted document verbatim into the GitHub
issue. It extracts the `## Summary` section only, then emits the bug template with every remaining
section filled in as the literal string `(not provided in potential file)`:

```
## Environment
(not provided in potential file)

## Steps to Reproduce
(not provided in potential file)
...
## Source
From: docs/features/potential/<name>.md
```

Verified 2026-08-22 on issue #584: the promoted document was ~90 lines carrying `## Root Cause`,
`## Impact`, `## Proposed Direction`, and `## Verification Notes`; the resulting issue body was 33
lines and contained exactly one of five content markers. Root-cause analysis, the counter-example
citation, the proposed remedy, and every `file:line` verification pointer were all dropped.

**Why this matters more than it looks.** The promotion lifecycle exists so that an out-of-scope
defect survives the archival of the feature folder that discovered it (see
[[feedback_promote_latent_defects_to_issues]]). If the local document is then deleted or never
committed — which is exactly the case inside an epic child, where the plan's hard constraints forbid
writing under `docs/features/potential/**` — the analysis is lost entirely and the issue retains only
a summary paragraph. The promotion appears to have succeeded while silently discarding the reasoning
that made it worth filing.

**How to apply.** After every `potential_to_issue` call, diff what you wrote against what landed:

1. `gh issue view <N> --json body -q '.body' | wc -l` and compare against the source document.
2. Grep the issue body for two or three distinctive markers from your analysis sections.
3. If content was dropped, post the missing sections as an issue COMMENT
(`gh issue comment <N> --body-file <file>`). A comment is durable, needs no repository write, and
does not disturb an audited diff.

Put the load-bearing content in `## Summary` when the document is short enough, or plan on the
follow-up comment when it is not. Do not assume the promoted markdown file is a durable second copy:
inside an agent worktree it is untracked and dies with the worktree.

Related: [[potential-to-issue-creates-github-issue]] (the tool opens the issue itself — never also
`gh issue create`), [[potential-to-issue-needs-absolute-path]],
[[promotion-potential-md-may-not-persist]].
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using System.Collections;
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.Office.Interop.Outlook;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace QuickFiler.Controllers.Tests
{
/// <summary>
/// Conversation-view coverage for <see cref="QfcExplorerController"/>: the
/// <c>ExplConvView_ToggleOn</c> and <c>ExplConvView_ToggleOff</c> state transitions and the
/// <c>GetSiblingView</c> lookup. Second part of the partial class declared in
/// <c>QfcExplorerControllerTests.cs</c>, split to keep both files under the 500-line limit.
/// <c>[TestClass]</c> stays on the base file only: it is <c>AllowMultiple = false</c>, so
/// repeating it here would be CS0579. Shares the base file's <c>Setup</c> fixture,
/// <c>CreateController</c>, and <c>ArrangeViewsIndexer</c> helpers. Deterministic — mocked COM
/// only, no live form, no message pump, no sleeps, no temporary files.
/// </summary>
public partial class QfcExplorerControllerTests
{
/// <summary>
/// <c>ExplConvView_ToggleOn</c> applies the remembered view and clears the flag when
/// <c>BlShowInConversations</c> is set.
/// </summary>
[TestMethod]
public void ExplConvView_ToggleOn_WhenFlagSet_AppliesRememberedView()
{
// Arrange
var rememberedView = _repo.Create<Outlook.View>();
var views = ArrangeViewsIndexer(rememberedView);
var currentFolder = _repo.Create<MAPIFolder>();
currentFolder.SetupGet(f => f.Views).Returns(views.Object);
_explorer.SetupGet(e => e.CurrentFolder).Returns(currentFolder.Object);

var controller = CreateController();
controller.BlShowInConversations = true;

// Act
controller.ExplConvView_ToggleOn();

// Assert
rememberedView.Verify(v => v.Apply(), Times.Once());
controller
.BlShowInConversations.Should()
.BeFalse("applying the remembered view completes the toggle-on transition");
}

/// <summary>
/// <c>ExplConvView_ToggleOn</c> is a no-op when <c>BlShowInConversations</c> is clear: the
/// negative branch must not touch the <c>Views</c> collection or apply any view.
/// </summary>
[TestMethod]
public void ExplConvView_ToggleOn_WhenFlagClear_DoesNothing()
{
// Arrange
var rememberedView = _repo.Create<Outlook.View>();
var views = ArrangeViewsIndexer(rememberedView);
var currentFolder = _repo.Create<MAPIFolder>();
currentFolder.SetupGet(f => f.Views).Returns(views.Object);
_explorer.SetupGet(e => e.CurrentFolder).Returns(currentFolder.Object);

var controller = CreateController();
controller.BlShowInConversations = false;

// Act
controller.ExplConvView_ToggleOn();

// Assert
views.Verify(v => v[It.IsAny<object>()], Times.Never());
rememberedView.Verify(v => v.Apply(), Times.Never());
controller.BlShowInConversations.Should().BeFalse();
}

/// <summary>
/// <c>ExplConvView_ToggleOff</c> is a no-op when conversations are not grouped: the guard reads
/// the command-bar state and returns without touching the current view.
/// </summary>
[TestMethod]
public void ExplConvView_ToggleOff_WhenConversationsNotGrouped_DoesNothing()
{
// Arrange — the fixture already arranges GetPressedMso to return false.
var controller = CreateController();
controller.BlShowInConversations = false;

// Act
controller.ExplConvView_ToggleOff();

// Assert
controller
.BlShowInConversations.Should()
.BeFalse("the negative guard must leave the flag untouched");
_explorer.VerifyGet(e => e.CurrentView, Times.Never());
}

/// <summary>
/// <c>ExplConvView_ToggleOff</c> copies the current view to a temporary conversation-free view,
/// strips the upgrade-to-conversation directive from its XML, saves it, and applies it, when no
/// sibling view of that name already exists.
/// </summary>
[TestMethod]
public void ExplConvView_ToggleOff_WhenSiblingViewMissing_CopiesAndSavesTemporaryView()
{
// Arrange — conversations ARE grouped, so the branch is entered.
_commandBars.Setup(c => c.GetPressedMso(ShowInConversationsMso)).Returns(true);

// A sibling collection with no view named tmpNoConversation, so GetSiblingView returns null.
var siblings = _repo.Create<Views>();
siblings
.Setup(v => v.GetEnumerator())
.Returns(() => ((IEnumerable)new List<Outlook.View>()).GetEnumerator());

var currentView = _repo.Create<Outlook.View>();
currentView.SetupGet(v => v.Name).Returns("Compact");
currentView.SetupGet(v => v.Parent).Returns(siblings.Object);
currentView
.SetupGet(v => v.XML)
.Returns("<view><upgradetoconv>1</upgradetoconv></view>");
_explorer.SetupGet(e => e.CurrentView).Returns(currentView.Object);

var temporaryView = _repo.Create<Outlook.View>();
currentView
.Setup(v =>
v.Copy(TemporaryViewName, OlViewSaveOption.olViewSaveOptionThisFolderOnlyMe)
)
.Returns(temporaryView.Object);

var controller = CreateController();

// Act
controller.ExplConvView_ToggleOff();

// Assert — copy, XML assignment with the directive stripped, save, then apply.
currentView.Verify(
v => v.Copy(TemporaryViewName, OlViewSaveOption.olViewSaveOptionThisFolderOnlyMe),
Times.Once()
);
temporaryView.VerifySet(v => v.XML = "<view></view>", Times.Once());
temporaryView.Verify(v => v.Save(), Times.Once());
temporaryView.Verify(v => v.Apply(), Times.Once());
controller
.BlShowInConversations.Should()
.BeTrue("toggling off records that conversations were grouped");
}

/// <summary>
/// <c>GetSiblingView</c> returns the sibling whose name matches the requested view name.
/// </summary>
[TestMethod]
public void GetSiblingView_WhenNamedViewPresent_ReturnsIt()
{
// Arrange
var other = _repo.Create<Outlook.View>();
other.SetupGet(v => v.Name).Returns("Compact");
var wanted = _repo.Create<Outlook.View>();
wanted.SetupGet(v => v.Name).Returns(TemporaryViewName);

var siblings = _repo.Create<Views>();
var list = new List<Outlook.View> { other.Object, wanted.Object };
siblings
.Setup(v => v.GetEnumerator())
.Returns(() => ((IEnumerable)list).GetEnumerator());

var currentView = _repo.Create<Outlook.View>();
currentView.SetupGet(v => v.Parent).Returns(siblings.Object);

var controller = CreateController();

// Act
Outlook.View result = controller.GetSiblingView(currentView.Object, TemporaryViewName);

// Assert
result.Should().BeSameAs(wanted.Object);
}

/// <summary>
/// <c>GetSiblingView</c> returns null when the loop is exhausted without a name match.
/// </summary>
[TestMethod]
public void GetSiblingView_WhenNamedViewAbsent_ReturnsNull()
{
// Arrange
var other = _repo.Create<Outlook.View>();
other.SetupGet(v => v.Name).Returns("Compact");

var siblings = _repo.Create<Views>();
var list = new List<Outlook.View> { other.Object };
siblings
.Setup(v => v.GetEnumerator())
.Returns(() => ((IEnumerable)list).GetEnumerator());

var currentView = _repo.Create<Outlook.View>();
currentView.SetupGet(v => v.Parent).Returns(siblings.Object);

var controller = CreateController();

// Act
Outlook.View result = controller.GetSiblingView(currentView.Object, TemporaryViewName);

// Assert
result.Should().BeNull();
}
}
}
Loading