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
2 changes: 2 additions & 0 deletions .claude/agent-memory/atomic-executor/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
- [Supersede clause leaves a hard routing residual](project_supersede_clause_leaves_hard_routing_residual.md) — a "the plan's table supersedes this" clause does not neutralise a THIRD location naming a concrete file; re-derive the arithmetic of obeying it
- [Verify line citations with numbered output](feedback_verify_line_citations_with_numbered_output.md) — never hand-count from a `sed` window; also verify the declared ANCHOR SHA (`git diff --name-only <ANCHOR> HEAD`), not only the numbers
- [Four recurring C# plan defect classes](project_preflight_recurring_csharp_plan_defect_classes.md) — omitted `.claude/rules/csharp.md` read, exact `dotnet --version` equality vs global.json rollForward, absolute `Failed: 0`, and a `Select-String -LineNumber` switch that does not exist
- [msbuild-log filename grep matches the csc command line](project_msbuild_log_token_search_matches_csc_command_line.md) — a byte-exact log line-set gate measures the compiler invocation, not diagnostics, and breaks on any added `<Compile Include>`
- [Epic integration base invalidates research line counts](project_epic_integration_base_invalidates_research_line_counts.md) — TestSupport.cs was 489 not 365 lines and every citation into it shifted +3; measure, never restate
- [Self-derived gate thresholds are blind](project_preflight_selfderived_gate_thresholds_are_blind.md) — a "count >= floor" gate whose floor comes from the runs it validates is deflation-blind + scope-incommensurable; use
- [Multi-pattern gates detach shared qualifiers](project_multipattern_gate_shared_qualifier_detachment.md) — rewriting one clause re-scopes the trailing allowlist to the last pattern only; restate the carve-out per pattern +
- [Merge-base diff gates need a commit cadence](project_preflight_mergebase_diff_gates_need_commit_cadence.md) — `<MERGE_BASE>..HEAD` gates are vacuous while HEAD == merge-base; plan an explicit commit task
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: epic-integration-base-invalidates-research-line-counts
description: Research measured against main is stale on an epic integration branch; TestSupport.cs was 489 not 365 lines, and every plan line citation into it was shifted by +3
metadata:
type: project
---

When a child feature branches from an **epic integration branch** rather than from `main`, every line
count and line citation the research document took against `main` may already be stale, because
sibling epic children have landed into the integration branch in the meantime.

Measured on #493 (2026-08-27), base `epic/quickfiler-bug-family-integration`:

- `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` was **489** lines, not the 365 both
research §8 and `spec.md` recorded from `main` at `988e819b`. Siblings had appended shared arrange
helpers carrying `Issue #480`, `Issue #483`, and `Issue #485` doc markers.
- Projected headroom was therefore 135 lines; **actual headroom was 11** against the 500-line ceiling.
- Every plan line citation into that file was shifted by a uniform **+3** (`238-249` was really
`241-252`; `221-222` was `224-225`; `213-220` was `216-223`).
- `QuickFiler.Test.csproj`'s `<Compile Include>` anchor was at line **157**, not the 146 the spec and
research cited.
- `Part2.cs`, `FocusAndThemeTests.cs`, and `UiThread.cs` matched their research figures exactly, so the
staleness is per-file, not global. Do not assume a uniform offset across files.

**Why:** research runs before the epic's other children merge. The plan inherits the research figures
as prose, and a plan whose gates restate a projection instead of re-measuring would have reported
headroom that does not exist.

**How to apply:** in Phase 0, measure line counts and locate every cited member by a line-numbered
search on its **identifier**, never by trusting the cited line range. Record the divergence and the
per-file offset in the file-inventory baseline artifact so later tasks are read against a disclosed
baseline. Apply edits by matching exact source text (Edit tool / `.Replace`), not by line offset. A
plan that already says "treat this AC as a fresh measurement, not a restatement of the projection"
(#493's Decisions Record D2) survives this intact; one that pins an absolute count does not.

Related: [[verify-line-citations-with-numbered-output]], [[exact-count-gate-vs-remediation-loop]],
[[stale-base-deletes-silently-on-fan-in]].
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
name: msbuild-log-token-search-matches-csc-command-line
description: A plan gate that greps an msbuild log for a source filename matches the csc.exe command line, not diagnostics, so byte-exact set equality breaks whenever the plan adds a Compile Include
metadata:
type: project
---

A plan gate of the form "extract every line of the msbuild log containing `<SomeFile>.cs`, then
assert the post-change line set is byte-identical to the baseline set" does **not** measure
diagnostics. At MSBuild's default verbosity the only lines containing a source filename are the
`csc.exe` command line emitted by `CoreCompile` and its companion `BuildResponseFile = '...'` echo.
Each is 30-60 KB and enumerates the compiling project's entire `/reference:` set **and** its entire
source-file set.

Consequences, all measured on #493 (2026-08-27):

- The **count** condition survives: adding source files lengthens an existing line rather than
adding one, so the count stays 2 per token per log.
- The **byte-exact set equality** condition fails for any token whose containing project gains a
`<Compile Include>` entry. Each affected line grew by exactly the length of the added path strings
(123 chars for two files). Symmetric difference at token granularity was exactly the two added
source-file arguments.
- A token belonging to a project the change does not touch (`UiThread.cs` in `UtilitiesCS`) keeps
byte-exact equality, so the gate is real for *that* file and vacuous-then-broken for the changed
project's files.

**Why:** the plan author intends the gate to discharge an AC clause like "no analyzer diagnostic is
raised at either call site", and reaches for a filename grep because a diagnostic line does contain
the filename. But so does the compiler invocation, and there is no diagnostic to find.

**How to apply:** at preflight, reject a byte-exact msbuild-log line-set gate scoped to a file in a
project the plan modifies. Substitute a gate on the diagnostic-bearing subset — partition the matches
on `warning <CODE>` / `error <CODE>` — or simply assert zero `warning CS` / `error CS` lines in the
whole log plus a byte-identity hash on the unowned file. During execution it is too late to block:
record the partition and the symmetric difference, state which sub-condition failed and why, and
check the AC off only if the criterion's own text is independently satisfied. Also record the hazard
in the **baseline** artifact before the change is made, so the later failure reads as disclosed rather
than as a surprise.

Do not paste the matched lines verbatim into a committed artifact: four lines is ~356 KB of reference
list. Record per line the log line number, redacted character length, and SHA-256 of the redacted
text, and write the full redacted lines to the git-ignored `TestResults/plan-logs/` tree so the
comparison stays byte-exact and reproducible.

Related: [[verify-line-citations-with-numbered-output]],
[[preflight-selfderived-gate-thresholds-are-blind]].
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 @@ -64,6 +64,7 @@
- [511-rescope-review-residuals](project_511-rescope-review-residuals.md) — 2026-08-24 re-audit PASS/0 blocking; residuals CR-1 stale RCA narrative + CR-2 AC-vs-deleted-TRX wording; PR must not close #511/#571 (#592/#594/#597 carry the real defects)

- [614-review-residuals](project_614-review-residuals.md) — cycle-2 exit NO-GO/1 blocking: RC-1 widened filing guard admits archive-root-exact row that RequireArchiveRelativeStem throws on, post-Hide async-void crash; CR-1 closed
- [493-review-residuals + msbuild-log gate adjudication](project_493-review-residuals-and-msbuild-log-gate-adjudication.md) — PASS/0 blocking; how a structurally unsatisfiable msbuild-log byte-equality gate was adjudicated without downgrading the AC

## Artifact hygiene
- [Never embed absolute host paths](../_shared_no_absolute_host_paths.md) — no `C:\Users\<account>\...`, bare account, or machine name in ANY artifact; use `<repo-root>` / `<user-profile>` / `<user>` / `<host>`. vstest names TRX `<account>_<HOST>_<ts>.trx` by default, so control `/ResultsDirectory:` + `LogFileName=` or rename before citing.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
name: 493-review-residuals-and-msbuild-log-gate-adjudication
description: '#493 review PASS/0 blocking; how the P4-T2 msbuild-log byte-equality gate failure was adjudicated (AC-6 still PASS); residuals CR-1 restore try/finally, CR-2 R2/R3 Ensure-race flake window, #648 open; UTC timestamps'
metadata:
type: project
---

#493 (quickfiler-test-uithread-dispatcher, epic child of quickfiler-bug-family) reviewed 2026-08-27T15-07: PASS, 0 Blocking, all 10 spec ACs PASS.

**Why:** the one genuine discrepancy was plan task P4-T2 — byte-exact set equality of msbuild-log lines containing `QfcItemController.FocusAndThemeTests.cs` failed because at default verbosity every matching line is a csc.exe invocation enumerating the project's whole source set, so the gate is structurally unsatisfiable whenever the plan itself adds compile items. Adjudication technique that settled it: delete exactly the added compile-input tokens from the final extract and `cmp` against the baseline extract (strip `\r` — a CRLF artifact broke the first compare); both analyzer and nullable extracts became byte-identical. AC judged on the spec criterion's own clauses (byte-identity via `git hash-object` vs base blob, zero diagnostic-bearing log lines, named tests passing), NOT on the plan's proxy; the checked-off-despite-literal-failure task recorded as a Non-blocking finding instead of downgrading the AC.

**How to apply:**
- Future plans must not gate on raw compiler-invocation text; if a reviewer sees such a gate fail, reproduce the token-removal normalization before accepting or rejecting the executor's story.
- Residuals to re-check at epic fan-in / later QuickFiler reviews: CR-1 — `PumpHarness.Restore` (Part2.cs) and `UiThreadDispatcherTransaction.Dispose` lack try/finally around restore-before-release (theoretical gate leak); CR-2 — R2/R3 assert absolute field values and can theoretically flake against a concurrent unowned `EnsureDispatcher` caller (design keeps Ensure off TransactionGate deliberately) — diagnose a rare R2/R3 flake as this, not a fixture defect; issue #648 (WpfUiDispatcherTests ungated swap, restores in finally) OPEN, out of scope by design.
- TaskMaster executor evidence timestamps are UTC while the local clock is -04:00; use `date -u` for review artifact timestamps so they sort after the evidence they cite.
- Coverage handling that passed the hook: no `artifacts/csharp/coverage.xml` emitted (deliberate, per delegation); C# coverage row written as FAIL (artifact-absence rule) dispositioned Non-blocking with the byte-identical Cobertura triple (0.19049434489769984 / 78690) as zero-delta proof — hook accepts because absent XML yields $null repoPct.
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using FluentAssertions;
using Microsoft.Office.Interop.Outlook;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Web.WebView2.Core;
Expand Down Expand Up @@ -33,22 +30,11 @@ public partial class QfcItemController_InitializationTests
{
// ------------------------- #230 pump-hosted initialization tests -------------------------

/// <summary>
/// Serializes the <c>UiThread.Dispatcher</c> swap across every pump-hosted test in this
/// assembly. Two separate test classes consume <see cref="BuildPumpHarnessAsync"/> — this
/// one and <c>QfcItemController_SeamFactoryTests</c> — and MSTest class-level
/// parallelization runs them concurrently. Without this gate one class's
/// <c>PumpHarness.Restore</c> can revert the process-wide static to the parked dispatcher
/// seeded by <c>QfcItemControllerTestSupport.EnsureUiThreadDispatcher</c> while the other
/// class's member under test is still awaiting a dispatcher operation; the parked
/// dispatcher never runs a frame, so that await never completes and the test fails on its
/// <c>[Timeout]</c> instead of on an assertion.
/// </summary>
/// <remarks>
/// The gate is a deterministic completion signal, not a wall-clock wait: <c>WaitAsync</c>
/// is released by the preceding test's <c>Restore</c>, never by elapsed time.
/// </remarks>
private static readonly SemaphoreSlim UiThreadDispatcherGate = new SemaphoreSlim(1, 1);
// The #230 serialization of the process-wide UiThread dispatcher now lives in
// UiThreadDispatcherFixture. It exists because two test classes consume the pump fixture and
// MSTest class-level parallelization runs them concurrently, so without it one class's restore
// can revert the static to a parked, never-pumped dispatcher while the other class is still
// awaiting a dispatcher operation, producing a timeout rather than an assertion failure.

/// <summary>
/// #230: builds a controller wired for a full initialization run against a real
Expand All @@ -64,21 +50,25 @@ bool darkMode
{
// Held until PumpHarness.Restore, so only one pump fixture owns the static
// UiThread.Dispatcher at a time across all test classes in this assembly.
await UiThreadDispatcherGate.WaitAsync().ConfigureAwait(false);
UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture
.BeginTransactionAsync()
.ConfigureAwait(false);
try
{
return await BuildPumpHarnessCoreAsync(host, darkMode).ConfigureAwait(false);
return await BuildPumpHarnessCoreAsync(host, darkMode, transaction)
.ConfigureAwait(false);
}
catch
{
UiThreadDispatcherGate.Release();
transaction.Dispose();
throw;
}
}

private static async Task<PumpHarness> BuildPumpHarnessCoreAsync(
WinFormsPumpHost host,
bool darkMode
bool darkMode,
UiThreadDispatcherTransaction transaction
)
{
QuickFiler.ItemViewer viewer = await host.InvokeAsync(() => new QuickFiler.ItemViewer())
Expand Down Expand Up @@ -135,26 +125,9 @@ bool darkMode
// complete an InvokeAsync. Point it at the pump thread's dispatcher (serviced by the
// WinForms loop, proven by WinFormsPumpHostTests.BothMarshalRoutes_*) for the duration
// of the test, and restore the previous value in PumpHarness.Restore so no state leaks.
Dispatcher previousUiThreadDispatcher = SwapUiThreadDispatcher(viewer.UiDispatcher);
transaction.Install(viewer.UiDispatcher);

return new PumpHarness(controller, viewer, cts, webView, previousUiThreadDispatcher);
}

/// <summary>
/// Replaces the static <c>UiThread._dispatcher</c> backing field and returns the previous
/// value, mirroring the reflection pattern in
/// <c>QfcItemControllerTestSupport.EnsureUiThreadDispatcher</c>.
/// </summary>
private static Dispatcher SwapUiThreadDispatcher(Dispatcher replacement)
{
FieldInfo field = typeof(UiThread).GetField(
"_dispatcher",
BindingFlags.NonPublic | BindingFlags.Static
);
field.Should().NotBeNull(because: "UiThread._dispatcher backing field must exist");
Dispatcher previous = (Dispatcher)field.GetValue(null);
field.SetValue(null, replacement);
return previous;
return new PumpHarness(controller, viewer, cts, webView, transaction);
}

/// <summary>
Expand Down Expand Up @@ -305,22 +278,22 @@ internal WebViewSentinelException()
/// </summary>
internal sealed class PumpHarness
{
private readonly Dispatcher _previousUiThreadDispatcher;
private readonly UiThreadDispatcherTransaction _transaction;
private bool _restored;

internal PumpHarness(
HarnessController controller,
QuickFiler.ItemViewer viewer,
CancellationTokenSource tokenSource,
Mock<IWebViewCoreInitializer> webViewInitializer,
Dispatcher previousUiThreadDispatcher
UiThreadDispatcherTransaction transaction
)
{
Controller = controller;
Viewer = viewer;
TokenSource = tokenSource;
WebViewInitializer = webViewInitializer;
_previousUiThreadDispatcher = previousUiThreadDispatcher;
_transaction = transaction;
}

internal HarnessController Controller { get; }
Expand All @@ -345,9 +318,11 @@ internal void Restore()
}

_restored = true;
SwapUiThreadDispatcher(_previousUiThreadDispatcher);
TokenSource.Dispose();
UiThreadDispatcherGate.Release();

// Disposing the transaction restores the captured previous value and only then
// releases the gate, so a waiter can never observe the pre-restore value.
_transaction.Dispose();
}
}

Expand Down
Loading
Loading