Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
12256da
docs(444): promote count-mismatch follow-up defect as issue #644
drmoisan Aug 27, 2026
9deb2be
docs(444): capture phase 0 baselines and verify upstream 468
drmoisan Aug 27, 2026
18a2463
fix(444): guard the KbdActions enumerable constructor against duplica…
drmoisan Aug 27, 2026
1118e5a
docs(444): check off the issue 444 acceptance criteria
drmoisan Aug 27, 2026
9494ca3
fix(472): unregister navigation keys at the width they were registered
drmoisan Aug 27, 2026
2bdfebf
docs(472): check off the issue 472 acceptance criteria
drmoisan Aug 27, 2026
a4c0c18
fix(482): route expansion registration through a single owner
drmoisan Aug 27, 2026
d0b0d49
docs(482): check off the issue 482 acceptance criteria
drmoisan Aug 27, 2026
ae7d948
docs(444): record P4-T1 final-loop formatting gate evidence
drmoisan Aug 27, 2026
3f373f5
Merge origin/epic/quickfiler-bug-family-integration into bug/quickfil…
drmoisan Aug 27, 2026
4027d78
chore(444): record the final toolchain and coverage evidence
drmoisan Aug 27, 2026
7c63e47
docs(444): check off the toolchain and coverage acceptance criteria
drmoisan Aug 27, 2026
ad924a9
docs(444): reconcile acceptance criteria and record deferred disposit…
drmoisan Aug 27, 2026
833423b
docs(444): record terminal clean-tree verification
drmoisan Aug 27, 2026
04b725b
docs(444): record feature-review policy, code and feature audit artif…
drmoisan Aug 27, 2026
b09565c
Merge origin/epic/quickfiler-bug-family-integration into bug/quickfil…
drmoisan Aug 27, 2026
51f929b
docs(444): record post-merge toolchain gate and close the final three…
drmoisan Aug 27, 2026
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: 1 addition & 1 deletion .claude/agent-memory/orchestrator/MEMORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
- [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
- [Preflight defect-trend scope confound](preflight-defect-trend-scope-confound.md) — a rising count isn't divergence if you widened scope that round; hold scope fixed one round, and track re-introductions not raw counts
- [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
- [potential_to_issue maps sections by heading name](potential-to-issue-keeps-only-summary-section.md) — template headings survive INTACT; only your own invented headings get dropped. Keep the scaffold's headings
- [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
- [WebView2 EndInit creates handles at construction](webview2-endinit-creates-handles-at-construction.md) — a bare ItemViewer already has both child handles AND its own; any "force the handle" remedy is a measured no-op
- [blocked_reason enum can't express a substantive halt](blocked-reason-enum-cannot-express-substantive-halt.md) — 7 mechanical members only; use "none" plus free-form halt/blocking_findings keys, never a wrong member
Expand Down
Original file line number Diff line number Diff line change
@@ -1,50 +1,55 @@
---
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
description: potential_to_issue maps sections by HEADING NAME against the bug template — template-matching headings survive verbatim, non-template headings are dropped and missing ones become "(not provided in potential file)"
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.
`mcp__drm-copilot__potential_to_issue` does not copy the promoted document verbatim, and it does not
unconditionally reduce it to `## Summary` either. It maps **section by section, matching on the
heading name** against the issue template it is emitting:

- A heading that matches a template section is carried through **with its full content intact**.
- A heading that does *not* appear in the template is **silently dropped**.
- A template section with no matching heading in the source is emitted as the literal string
`(not provided in potential file)`.

The scaffolded potential-bug template states this itself, in a line that is easy to skim past:
"Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them
into the GitHub bug issue template."

**Why the earlier reading was wrong.** This memory previously recorded the rule as "keeps ONLY the
Summary section", generalised from issue #584 on 2026-08-22. That document carried `## Root Cause`,
`## Impact`, `## Proposed Direction` and `## Verification Notes` — **none** of which is a bug-template
heading. Every one was dropped for that reason, and `## Summary` survived only because it happens to
be a template heading. The observation was correct; the causal explanation was not, and the wrong
explanation predicts data loss even when there will be none.

Counter-example, verified 2026-08-27 on issue #644 (`qfc-unregister-navigation-count-mismatch-orphan`,
the AC-472-10 follow-up promoted out of epic child 444): the source was created with
`new_potential_bug_entry`, its scaffolded headings were left unchanged, and **all ten sections landed
in the issue body with full content** — `## Summary`, `## Environment`, `## Steps to Reproduce`,
`## Expected Behavior`, `## Actual Behavior`, `## Logs / Screenshots`, `## Impact / Severity`,
`## Suspected Cause / Notes`, `## Proposed Fix / Validation Ideas`, `## Next Step`. No follow-up
comment was needed.

**How to apply.**

1. Scaffold with `new_potential_bug_entry` / `new_potential_entry` and **fill the template headings
in place**. Do not invent your own section names — that, not the tool, is what loses content.
2. When analysis has no template home, fold it into the nearest template section (root cause fits
`## Suspected Cause / Notes`; remedy fits `## Proposed Fix / Validation Ideas`) rather than adding
a new heading.
3. Still verify after every call: `gh issue view <N> --json body` and grep for two or three
distinctive markers. Confirm, don't assume — in either direction.
4. If content was genuinely dropped, post the missing sections as an issue COMMENT
(`gh issue comment <N> --body-file <file>`): durable, needs no repository write, does not disturb
an audited diff.

Do not rely on the promoted markdown as a durable second copy: inside an agent worktree it is
untracked and dies with the worktree unless committed. Note also that a source resolved directly from
`docs/features/potential/` is **moved** into `promoted/`, not copied.

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]].
[[promotion-potential-md-may-not-persist]], [[feedback_promote_latent_defects_to_issues]].
91 changes: 91 additions & 0 deletions QuickFiler.Test/Controllers/KbdActionsRemainingBranchesTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using FluentAssertions;
Expand Down Expand Up @@ -163,6 +164,96 @@ public void Enumeration_YieldsAllRegisteredInstancesAndKeysProjection()
registry.Keys.Should().BeEquivalentTo(new[] { Keys.Enter, Keys.Escape });
}

/// <summary>
/// Issue #444: the enumerable constructor must enforce the same (SourceId, stored Key)
/// duplicate guard both Add overloads enforce, so an inconsistent registry cannot be seeded
/// past the invariant and surface later as an ambiguous Find.
/// </summary>
[TestMethod]
public void EnumerableConstructor_WhenSeedContainsDuplicateSourceAndStoredKey_ThrowsArgumentException()
{
// Arrange
var seed = new List<KaKey>
{
new KaKey("src", Keys.Down, _ => { }),
new KaKey("src", Keys.Down, _ => { }),
};

// Act
Action act = () => new KbdActions<Keys, KaKey, Action<Keys>>(seed);

// Assert
act.Should()
.Throw<ArgumentException>(
"a seed sequence repeating one source and stored key violates the registry invariant"
)
.WithMessage("*already exists*");
}

/// <summary>
/// Issue #444: the duplicate guard materialises the seed before scanning it, so a null
/// sequence must still surface as ArgumentNullException from the List copy constructor and
/// never as NullReferenceException from the scan.
/// </summary>
[TestMethod]
public void EnumerableConstructor_WhenListIsNull_ThrowsArgumentNullException()
{
// Arrange
List<KaKey> seed = null;

// Act
Action act = () => new KbdActions<Keys, KaKey, Action<Keys>>(seed);

// Assert
act.Should()
.Throw<ArgumentNullException>(
"the seed is materialised before it is scanned, so the null check is unchanged"
);
act.Should().NotThrow<NullReferenceException>();
}

/// <summary>
/// Issue #444: a duplicate-free seed is still accepted, so the guard rejects only genuine
/// invariant violations.
/// </summary>
[TestMethod]
public void EnumerableConstructor_WhenSeedIsDuplicateFree_DoesNotThrow()
{
// Arrange
var seed = new List<KaKey>
{
new KaKey("src", Keys.Up, _ => { }),
new KaKey("src", Keys.Down, _ => { }),
};

// Act
Action act = () => new KbdActions<Keys, KaKey, Action<Keys>>(seed);

// Assert
act.Should().NotThrow("two distinct keys under one source do not collide");
}

/// <summary>
/// Issue #444: the guard keys on the (SourceId, Key) pair, not on the key alone, so one key
/// repeated under two different sources remains legal exactly as it is through Add.
/// </summary>
[TestMethod]
public void EnumerableConstructor_WhenSameKeyUnderDifferentSourceIds_DoesNotThrow()
{
// Arrange
var seed = new List<KaKey>
{
new KaKey("sourceA", Keys.Down, _ => { }),
new KaKey("sourceB", Keys.Down, _ => { }),
};

// Act
Action act = () => new KbdActions<Keys, KaKey, Action<Keys>>(seed);

// Assert
act.Should().NotThrow("the guard compares the source and the key together");
}

[TestMethod]
public void FilterKeys_ReturnsOnlyMatchingInstances()
{
Expand Down
37 changes: 37 additions & 0 deletions QuickFiler.Test/Controllers/KbdActionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,43 @@ public void Add_WhenSourceAndStoredKeyAreExactDuplicate_ThrowsArgumentException(
.WithMessage("*already exists*");
}

/// <summary>
/// Issue #444: pins the enumerable constructor's duplicate guard to StoredKeyEquals rather
/// than the element-defined KeyEquals. KaStringAsync.KeyEquals matches on a substring, so a
/// KeyEquals-based guard would reject the legally coexisting "10" and "1" pair this file
/// already characterises for Add, and would fire that method's gated side effects during
/// construction. This test fails if a future edit swaps the guard to KeyEquals.
/// </summary>
[TestMethod]
public void EnumerableConstructor_WhenStoredKeysDifferButKeyEqualsOverlaps_DoesNotThrow()
{
// Arrange
var seed = new[]
{
new KaStringAsync
{
SourceId = "Collection",
Key = "10",
Delegate = _ => Task.CompletedTask,
},
new KaStringAsync
{
SourceId = "Collection",
Key = "1",
Delegate = _ => Task.CompletedTask,
},
};

// Act
Action act = () => new KbdActions<string, KaStringAsync, Func<string, Task>>(seed);

// Assert
act.Should()
.NotThrow(
because: "the constructor guard compares stored keys, so substring-overlapping distinct keys remain legal"
);
}

[TestMethod]
public void FilterKeys_WhenDistinctStoredKeysCoexist_PreservesKeyboardMatchingSemantics()
{
Expand Down
Loading
Loading