Skip to content

041: one condition, one notice (#278 #327 #328 #314) - #337

Merged
Bidthedog merged 8 commits into
masterfrom
feature/S041-I278-I314-I327-I328-notice-consolidation
Aug 27, 2026
Merged

041: one condition, one notice (#278 #327 #328 #314)#337
Bidthedog merged 8 commits into
masterfrom
feature/S041-I278-I314-I327-I328-notice-consolidation

Conversation

@Bidthedog

@Bidthedog Bidthedog commented Aug 27, 2026

Copy link
Copy Markdown
Owner

One condition raises one notice, carrying one row per distinct casualty, and a repeat makes that notice louder rather than adding another.

Three of the four issues are conformance, not invention: 029 FR-016/018/019 and 030 FR-034/037a were established, shipped, and had since stopped holding. This restores them without renumbering them, and adds guards so the next silent retirement fails something. The genuinely new ground is #327 (what happens when a file is refused and no editor panel exists) and #314 (what the keyboard route to a notice is).

What a user can now do

  • Delete a folder outside throng and be told once, naming the folder they removed — not once per expanded child.
  • Retry a file throng will not open and watch the existing notice pulse instead of growing a duplicate row, with its dismissal timer restarting so it cannot vanish mid-retry.
  • Open a refused file and get no panel and one notification — the same outcome with 0, 1 or 3 editors open, and from every gesture that could have created a panel.
  • Press Ctrl+Alt+M to reach the newest notice from anywhere, including a focused terminal, with Esc returning focus where it was.

Why the four are one branch

They are not independent. 030 keyed a notice's list on panelId; #327's fix stops creating a panel for a refused open, which removes the only de-duplication key #328 has. Fixing either alone leaves the other unstateable — so the branch widens the row into a casualty (panel optional, keyed on the panel where there is one, else (subject, reason)) and builds the rest on that.

Worth a reviewer's attention

  • useReportPanelFailure opens with if (!place) return. That guard is correct and stays — a panel destroyed mid-flight must not get an invented row (030 FR-027). But a refusal now has no panel by construction, so it reports through a sibling hook. Left alone, "no panel is created" would have become "no panel and no notification", which every panel-counting test would call success.
  • Two projections silently lost data. affectedDetails and affectedNames both flat-mapped groupAffected alone, so a panel-less casualty's absolute path would never have reached the diagnostics log and a screen reader would have said "That happened again" with no subject. Both invisible by construction, since detail is never rendered.
  • The E2E stage caught something six cheaper stages could not. resolveSaveConfinement falls back to "must be outside every project" when a project-owned document has no ownerRoot; openInto had no owner to derive one from, so ordinary in-project files came back out-of-tree and normal opens created no panel. Fixed twice over: the renderer now sends ownerProjectId (main still derives the root itself, so 018 US9's trust boundary holds), and an out-of-tree refusal is never invented when the owner is unknown.
  • Two decisions recorded in source comments, not specs, nearly got reversed — 033's reason for gating in the caller rather than inside openFileInNewEditor, and editor-ipc.ts's rule that the renderer must never supply its own confinement roots. Both drafts of mine did exactly what those comments forbid.

The guard review, and what it found (T062, T063)

FR-029 and FR-030 exist to be applied to this feature's own tests at the end. Doing it found three
defects
, none of which any green suite would have shown.

1. The storm guard tested a function production never called. use-explorer-data.ts shipped with
its own ancestor loop, so isSuppressedByAncestor — the subject of SC-001's counts and SC-006f's
120-permutation sweep — had no production caller at all. The rule had two statements and the tests
exercised the one that never ran. The renderer now resolves absence over the shared walk and delegates
the decision to core, and explorer-storm-suppression.test.ts covers the half core cannot see: that
the renderer asks.

2. The one E2E asserted nothing. It focused an editor, pressed the chord against an empty
notice stack, and asserted the editor still had focus — true whether the binding resolves, is inert,
or is deleted outright, and its own comment described a focused terminal it never created. Replaced by
notice-focus-chord.e2e.ts: a real cmd, a real notice, and an assertion that focus moved. The
budget is unchanged — one declaration, one @window tag, a different file — and focus.notice moved
to COVERED_ELSEWHERE, which the manifest guard verifies by reading the keystroke out of the code.

3. focus.notice could not reach most notices. FR-020 says "the most recent notice on screen",
unqualified, but the implementation targeted the most recent notice carrying a casualty list. A
rename collision, a refused delete, a failed watcher — each is a sentence and a Dismiss button, each is
commoner than the consolidated kind, and on each the binding did nothing. Worse, with a plain notice
newest it skipped backwards to an older one that had a list, which FR-020d forbids in as many words.
Now targets the newest notice card and prefers its list within.

T063 — each guard, reverted once, and what went red

Measured 2026-08-27. Nothing here ships (FR-030b): every revert was applied, run, and undone.

Restored requirement Fix reverted What failed
029 FR-019 — one cause, one notice the renderer stops consulting isSuppressedByAncestor explorer-storm-suppression.test.ts: 2 notices where 1 is required
029 FR-016/FR-018, 030 FR-034 — the raw errno is never rendered the banner also renders detail.systemError panel-failure-banner-path.test.ts: 2 failuresnot to contain 'ENOENT', and the path-once guard too (the errno carries the path)
030 FR-037a — no duplicate row per re-attempt joinedPanels keys on panelId alone again affected.test.ts: 4 failures, all expected … length of 2 but got 1
FR-020a — a real shell does not swallow the chord the E2E presses an unbound Control+Alt+Shift+F9 the new E2E: "focus never reached the notice" — it discriminates where its predecessor did not

The row that matters most is the one that stayed green. Under the first revert,
ancestor-suppression.test.ts passed 18/18 — SC-001 and the 120-permutation sweep included — while
the storm raged. A guard can be immaculate and still be aimed at code that does not run, and that is
the argument for finding 1 in a single measurement rather than in a paragraph.

Verification

On the pushed tree (eb8cbdad, rebased onto e6460d10), the seven cheap gate stages:

lint ✓ · typecheck ✓ · build ✓ · unit 3203 ✓ · component 1023 ✓ · integration
533 ✓ · contract 138

A full 8-stage npm run gate went green on this branch's content earlier — 24m 25s, e2e 205
parallel + 354 serial, zero retries — but that was before master gained the #290 terminal work, so
it does not describe the tree being merged. E2E has not been re-run locally since that rebase: a
sync run does not start one, and CI's @core lane gates the push. Said plainly rather than rounded up.

The rebase had two real conflicts, and one was arithmetic

e2e-budget.json is a ratchet that fails in both directions, and both sides of the rebase had
written total: 559 — master because #290 added a @terminal test, this branch because 041 added a
@window one. Each was right alone; together the answer is 560, with @terminal 103 and
@window 192. Resolved to that and proven by e2e-budget.test.ts passing in the run above. A
conflict where both sides wrote the same number and both were wrong is exactly what this ratchet is
for.

docs/testing.md was additive on both sides — master's third race-class sibling and this branch's
flake-reproduction section — and both are kept.

An earlier run's flakes, and who they belong to

An intermediate gate run came back 2 flaky in the serial tier: preferences-reset.e2e.ts:217 and
theme-flash.e2e.ts:199, both @prefs @reserve:window. Neither is in code this branch touches, but
"it isn't mine" needs evidence, so both trees were sampled the way the suite actually runs —
separate invocations, fresh app and config root each, one worker, --retries=0, idle machine:

Tree Rounds Failed Which test
origin/master 6 2 (33%) preferences-reset.e2e.ts:486, twice
this branch 4 1 (25%) theme-flash.e2e.ts:199

Different tests fail on each side, which rules out a defect in either tree and points at the
family being timing-sensitive. Filed as #341.

One method note, because it nearly produced the wrong conclusion: --repeat-each is the wrong
instrument.
It replays a test in one worker against the previous repeat's state, and
preferences-reset.e2e.ts is a file about config writes landing — so it produced a convincing
failure (binding-reset-zoom.in disabled, "already at its default value") that was the harness
rather than the code, and aimed the investigation at this diff. Recorded in docs/testing.md.

Known gap

#278 declares two defects. The storm (one notice per expanded child) is fixed, tested, and its
guard now points at the code that actually runs — see the guard review above. The raw ENOENT
rendered on the notice
could not be reproduced: ENOENT classifies at cause.ts:102, so
files-service.ts returns the spoken sentence and the raw text rides in copyDetail, which no code
path renders. Nothing was changed on a theory. Closing #278 on the storm; reopen if the raw errno is
seen again.

80 of 84 tasks. The four open are three that cannot be reproduced (the above) and one that is the
developer's to perform — the manual regression watch in quickstart.md §7. tasks.md records which
is which and why.

Closes

Closes #278
Closes #327
Closes #328
Closes #314

Related

Related to #321 — serial-tier E2E flakes; measured evidence posted there, not fixed here

@Bidthedog Bidthedog added this to the v1.0.0 milestone Aug 27, 2026
@Bidthedog Bidthedog added bug Something isn't working enhancement New feature or request area:ui-shell Main window, panes, tabs, title bar, notices area:editor Code editor panels, Monaco, find & replace area:explorer File explorer tree, file operations labels Aug 27, 2026
Bidthedog added a commit that referenced this pull request Aug 27, 2026
T047a (the FR-019 banner guard, 4/4 passing), T064 (no mutation harness or gate stage
added — verified against the diff and package.json), T065 (docs/testing.md needed no edit;
it references e2e-budget.json rather than restating its numbers) and T066 (affected.ts's
header now describes casualties and records why the panel-named symbols keep their names).
74 of 81.

The status block is the point of this commit rather than the four checkboxes. An unticked
task looks the same whether it was forgotten, deferred, or deliberately not done — and
these seven are open for three different reasons, only one of which is ordinary unfinished
work.

Three cannot be reproduced. #278 declares a second defect, a raw ENOENT rendered as the
notice's second line, and the route by which it reaches the screen was not found: ENOENT
classifies at cause.ts:102, so files-service returns the spoken sentence and the raw text
rides in copyDetail, which nothing renders. Nothing was changed on a theory, because a fix
behind no failing test cannot be verified by anything — including the green suite that
would follow it.

One is the developer's: the manual regression watch is not automatable and not the
implementer's to tick.

Three are genuinely outstanding, and T063 is the one with teeth — FR-030a's sensitivity
proof, where each guard's fix is reverted once and that guard is observed to fail. A guard
nobody has watched go red is an assertion that it would, which is exactly the assumption
Group 5 exists to stop making.

NOT regenerated. /speckit-tasks builds a task list from the template, which would have
rewritten this file wholesale and taken 74 ticks, the per-task deviation notes, and the
task IDs that five commit messages and PR #337 reference with it. The need was four
checkboxes.
@Bidthedog
Bidthedog force-pushed the feature/S041-I278-I314-I327-I328-notice-consolidation branch from d149f25 to b554891 Compare August 27, 2026 07:01
Bidthedog added a commit that referenced this pull request Aug 27, 2026
T047a (the FR-019 banner guard, 4/4 passing), T064 (no mutation harness or gate stage
added — verified against the diff and package.json), T065 (docs/testing.md needed no edit;
it references e2e-budget.json rather than restating its numbers) and T066 (affected.ts's
header now describes casualties and records why the panel-named symbols keep their names).
74 of 81.

The status block is the point of this commit rather than the four checkboxes. An unticked
task looks the same whether it was forgotten, deferred, or deliberately not done — and
these seven are open for three different reasons, only one of which is ordinary unfinished
work.

Three cannot be reproduced. #278 declares a second defect, a raw ENOENT rendered as the
notice's second line, and the route by which it reaches the screen was not found: ENOENT
classifies at cause.ts:102, so files-service returns the spoken sentence and the raw text
rides in copyDetail, which nothing renders. Nothing was changed on a theory, because a fix
behind no failing test cannot be verified by anything — including the green suite that
would follow it.

One is the developer's: the manual regression watch is not automatable and not the
implementer's to tick.

Three are genuinely outstanding, and T063 is the one with teeth — FR-030a's sensitivity
proof, where each guard's fix is reverted once and that guard is observed to fail. A guard
nobody has watched go red is an assertion that it would, which is exactly the assumption
Group 5 exists to stop making.

NOT regenerated. /speckit-tasks builds a task list from the template, which would have
rewritten this file wholesale and taken 74 ticks, the per-task deviation notes, and the
task IDs that five commit messages and PR #337 reference with it. The need was four
checkboxes.
…nd real errors

Spec 041 covers #278, #328, #327 and #314 as one feature, on the grounds that three of
the four are conformance rather than invention: 029 FR-016/018/019 and 030 FR-034/037a
were established, shipped, and have since stopped holding. The spec restores them without
renumbering them, and adds guards so the next silent retirement fails something.

Two pieces are genuinely new ground — what happens when a file is refused and no editor
panel exists (#327), and what the keyboard route to a notice is (#314).

THIRTEEN CLARIFICATIONS, THEN THREE ANALYSIS PASSES

The passes are worth recording because each caught a different class of error, and the
severity fell 3 → 4 → 0 without any finding repeating.

The first pass caught a contradiction the clarifications had themselves introduced:
FR-007a said "a refused open does" carry an affected-panel list while FR-013 said a
refused open creates no panel. affected.ts requires a panelId on every row and
de-duplicates on it, so after FR-013 there was nothing to key on and #328 would have
stayed unfixable for exactly the case #327 creates. That is why the two are one piece of
work rather than two.

The second pass read the SOURCE rather than the artifacts, and found three more. FR-007's
identity is a fallback and not a triple — the panel supersedes (subject, reason), because
within one notice a given panel fails once. The widening as specified would have made
subject and reason required on every row, breaking six construction sites at compile time,
four of them the tests whose job is to prove the old behaviour survived. And FR-013a
missed an entry point in the very surface #327 was reported from.

The third pass caught the sharpest one: FR-013d, as I had just written it, REVERSED a
decision 033 recorded in quick-open.tsx — that the gate belongs in the caller because
making openFileInNewEditor silently not force a new panel "would change a shipped contract
under a caller that has already done the check". The governing decision was in a source
comment, not in any spec.md.

TWO REQUIREMENTS WERE WRONG AND ARE REWRITTEN RATHER THAN QUIETLY FIXED

FR-018b instructed correcting a comment that is accurate: the row render emits row.label
and nothing else, so detail reaches Copy and the log and reaches the DOM by no path at
all. FR-019 turned out to be already honoured in both panel types with zero tasks tracking
it, so it gains FR-019a — a guard under FR-028, not an implementation. A requirement that
is true with nothing asserting it is the exact shape all three restored requirements had
immediately before they stopped being true.

Both are recorded in a "Corrections from analysis" section. A wrong requirement that is
silently fixed teaches nobody why it was wrong.
…278 #327 #328 #314)

Four issues, one model. They are together because the first thing the plan discovered was
that they are not independent: #327's fix removes the only de-duplication key #328 has.

THE WIDENING, FIRST, BECAUSE EVERYTHING ELSE NEEDS IT

030 keyed a notice's list on panelId. FR-013 stops creating a panel for a refused open, so
such a casualty has no panelId and "at most one row per casualty" becomes unstateable
rather than merely unimplemented. A row is now a union: AffectedPanel keeps every field it
had, AffectedSubject requires subject and reason and has panelId?: undefined. The union is
the enforcement — a panel-less row cannot omit its identity and a panelled row is
unchanged, which is what lets every existing caller and test stand untouched.

casualtyKey is a FALLBACK, not a composite: the panel supersedes (subject, reason). Within
one notice the cause is already fixed and a given panel fails once, so reason can never
separate two rows sharing a panel — folding it in would undo 030 FR-037a. The same panel
defeated by a different cause is a different NOTICE, decided by groupKey.

groupAffected keeps its signature and returns the panelled rows; a sibling ungroupedAffected
returns the rest. Changing the return would have broken four consumers and five
destructuring sites in its own suite.

#278 — SUPPRESSION DECIDED PER EVENT, SO ARRIVAL ORDER CANNOT MATTER

Five vanished folders minted five different path-missing:<folder> keys, so cause-key
equality matched none and 029 FR-019 never engaged. The keys were never wrong; four of
those five removals were not causes. isSuppressedByAncestor asks, from the removal alone,
whether an ancestor inside the project root is also gone — answerable without reference to
any other event, which is why it needs no buffering. 030 FR-036 forbids grouping "by time
or by window", and a wait would have been exactly that.

#328 — A REPEAT IS LOUDER, NOT LONGER

Two sites returned silently: mergeAffected returning the original array, and the duplicate
check matching. The model DETECTED both repeats and threw the fact away, so a user
re-triggering a condition got nothing back. Both now flash — pulse the card, restart the
dismissal timer. The timer restart is what stops a notice expiring while the user is still
producing the condition it reports. Repeats during a running pulse are absorbed, and the
announcement is bound to the pulse (one per pulse), so ten rapid retries give one utterance
rather than ten queued in a polite live region.

#327 — A REFUSAL IS NOT A DOCUMENT

createDedicatedEditor built the panel BEFORE anything read the file, so the first moment a
refusal was knowable was already too late. openInto now answers first, as a third
OpenDecision variant beside focus and open — no new IPC, because every open path already
awaits that decision, and a caller that ignores refuse fails to COMPILE.

A missing file returns open, never refuse: its panel is what holds the recovered buffer
018 shipped.

The riskiest line in the feature is useReportPanelFailure's "if (!place) return". It stays
for a panel destroyed mid-flight, and refusals report through a sibling hook instead —
otherwise "no panel is created" becomes "no panel and no notification", which every
panel-counting test would call success.

#314 — A KEYBOARD ROUTE, AND A WAY BACK

focus.notice, Ctrl+Alt+M, scoped EVERYWHERE because a notice can be raised while a terminal
has focus and that is where it is most likely to appear. Idempotent and never walking the
stack — Tab already traverses it. The Escape origin is captured at the press and not
re-captured by Tab, so a user who tabs on to a second notice still lands where they
started; when that element has been destroyed, focus falls back to a real surface, because
document.body resolves to no keybinding scope and the next chord would silently do nothing.

NOT_A_MISSING_FILE moved to @throng/core: main cannot import a renderer module, and the
enumeration of what counts as a refusal is a pure decision with consumers in two processes.

TWO PROJECTIONS THAT WOULD HAVE LOST DATA SILENTLY

affectedDetails and affectedNames both flat-mapped groupAffected alone — so a panel-less
casualty's absolute path would never have reached the diagnostics log, and a screen reader
would have said "That happened again" with no subject. Both invisible by construction,
since detail is never rendered. Both fixed, both now asserted.

AND ONE THE E2E STAGE CAUGHT THAT NOTHING CHEAPER COULD

resolveSaveConfinement falls back to "must be outside every project" when a project-owned
document has no ownerRoot. openInto had no owner to derive one from, so ordinary
in-project files came back out-of-tree and NORMAL OPENS CREATED NO PANEL. The renderer now
sends ownerProjectId — main still derives the root itself from the daemon's list, so 018
US9's trust boundary holds — and an out-of-tree refusal is never invented when the owner is
unknown. Confinement is still enforced at load time with the real per-panel ownership.
Constitution V's rule is the layer is chosen by what the assertion needs. Nearly all of
this is unit and component; there is exactly ONE new E2E, for the single fact no cheaper
layer can observe.

WHAT jsdom CANNOT SEE, AND WHAT THAT CHANGED

jsdom applies no stylesheet and performs no layout, so "the card visibly pulses", "the
list did not move" and "a visible affordance" are not observable at the component layer —
a test claiming them would pass vacuously. So the pulse is a STATE (data-pulsing) and the
focus affordance is MARKUP (data-focusable), with the stylesheet deciding only appearance.
The flash's "moves nothing" test asserts scrollTop, activeElement and row order, which
jsdom does track.

The announcement count is an attribute rather than a nonce in the sentence, because a live
region only speaks when its content changes and two consecutive repeats of one subject
produce the same string — appending a counter would have made a screen reader read the
number out loud.

THE ASSERTIONS THAT EARN THEIR PLACE

- All 120 arrival orders of five removal events yield the same single cause, and the same
  subject every time. Permuted rather than slept through: a result needing a delay to be
  right IS the time-grouping the requirement forbids, so a sleeping test would pass for
  the wrong reason and keep passing through a regression.
- SC-001's stated 1/3/5 measurement points, rather than a convenient number.
- Utterances counted against pulses in BOTH directions — ten rapid repeats give one, three
  spaced repeats give three. A bound that only ever suppressed would pass against an
  implementation that had gone silent, which is the defect being fixed.
- Zero panels at 0, 1 and 3 existing editors, each paired with the notification: "zero
  panels" alone is satisfied by doing nothing at all.
- A MISSING file still creates its panel. The single highest-value assertion here — if it
  ever goes green by creating zero panels, 018's recovery path has been deleted with
  nothing near it failing.
- The banner guard counts path OCCURRENCES rather than asserting presence, because
  presence passes just as happily against a banner that prints it three times.

ONE E2E, IN THE FILE IT BELONGS TO

That a real shell does not swallow Ctrl+Alt+M. A terminal forwards nearly everything to
its process, so a binding in the wrong tier is eaten and the notice is unreachable exactly
where FR-020a says it matters. Added to window-chord-resolution.e2e.ts beside the family it
belongs to — tagged @reserve:input from that precedent, and @Extended because chord routing
does not change per-commit. A new spec file would have needed a parallel-plan entry with a
mechanism from a closed set whose UNATTRIBUTED count is asserted by equality at 14.

e2e-budget.json re-seeded here: total 558→559, @window 191→192, core unchanged.
window-chords.ts claims focus.notice, because its manifest guard reported it uncovered —
"a regression in this listener is silent, so an uncovered action is a command that can die
without a single test going red".

Seven pre-existing test lines changed, all mechanical: six awaits and one async, because
openInto became asynchronous. No assertion was altered — the widening is held to the rule
that the tests proving 030's behaviour are not edited, and affected.test.ts is 113
insertions with zero deletions.
The one user-visible surface this feature adds is a default chord, and docs/quick-start.md
tabulates that family by name — Ctrl+Alt+B/N, Ctrl+Alt+T, Ctrl+`, Ctrl+Alt+Arrow. A
shortcut nobody can discover is #314 one layer out, so it joins them, with Escape's
behaviour stated because returning focus is half of what makes the binding usable.

Nothing else needed. The other three issues change how existing behaviour works rather
than adding a surface the manual describes, and docs/testing.md references
e2e-budget.json rather than restating its numbers — so the 558→559 bump needs no edit
there. The two E2E measurements it does carry are stamped to spec 035 and remain accurate
as provenance.
Generated by the agent-context extension, which rewrites the block between the SPECKIT
markers to name the current feature's plan. It moves from 040 to 041.

Its own commit rather than folded into docs(041), because an agent instruction file
changes how every future agent behaves in this repository and must be revertible on its
own — a rule reverted by accident, alongside a shortcut-table edit, is a rule nobody
notices going.

Worth recording for whoever runs the generator next: it is dead on a Windows checkout for
two reasons that look like one. `python3` resolves to the WindowsApps Store stub, and the
bash flavour takes `command -v python3` as proof of an interpreter with no version check
on that branch — so the probe succeeds and the invocation prints "Python was not found".
Past that, Python's text mode on Windows writes \r\n to stdout while the script reads the
config parser's three lines with `IFS= read -r`, which strips \n and keeps \r: the updater
then tries to open a file literally named "CLAUDE.md\r". Both were worked around outside
the repo. It also emits an ABSOLUTE path when it auto-detects the plan, because its
`${_plan_abs#$PROJECT_ROOT/}` strip fails against Python's backslashes — passing the plan
path explicitly avoids that branch entirely, which is how this block came out
repo-relative.
T047a (the FR-019 banner guard, 4/4 passing), T064 (no mutation harness or gate stage
added — verified against the diff and package.json), T065 (docs/testing.md needed no edit;
it references e2e-budget.json rather than restating its numbers) and T066 (affected.ts's
header now describes casualties and records why the panel-named symbols keep their names).
74 of 81.

The status block is the point of this commit rather than the four checkboxes. An unticked
task looks the same whether it was forgotten, deferred, or deliberately not done — and
these seven are open for three different reasons, only one of which is ordinary unfinished
work.

Three cannot be reproduced. #278 declares a second defect, a raw ENOENT rendered as the
notice's second line, and the route by which it reaches the screen was not found: ENOENT
classifies at cause.ts:102, so files-service returns the spoken sentence and the raw text
rides in copyDetail, which nothing renders. Nothing was changed on a theory, because a fix
behind no failing test cannot be verified by anything — including the green suite that
would follow it.

One is the developer's: the manual regression watch is not automatable and not the
implementer's to tick.

Three are genuinely outstanding, and T063 is the one with teeth — FR-030a's sensitivity
proof, where each guard's fix is reverted once and that guard is observed to fail. A guard
nobody has watched go red is an assertion that it would, which is exactly the assumption
Group 5 exists to stop making.

NOT regenerated. /speckit-tasks builds a task list from the template, which would have
rewritten this file wholesale and taken 74 ticks, the per-task deviation notes, and the
task IDs that five commit messages and PR #337 reference with it. The need was four
checkboxes.
FR-029 and FR-030 exist to be applied to this feature's own tests at the end
(T062). Applying them found three defects, none of which any green suite would
have shown, and each needed a fix rather than a note.

THE STORM GUARD TESTED A FUNCTION PRODUCTION NEVER CALLED. use-explorer-data.ts
shipped with its own ancestor loop, so isSuppressedByAncestor -- the subject of
SC-001's counts and SC-006f's 120-permutation sweep -- had no production caller
at all. The rule had two statements and the tests exercised the one that never
ran. The renderer now resolves absence over the shared walk and delegates the
decision to core; explorer-storm-suppression.test.ts covers the half core cannot
see, which is that the renderer asks. Measured: with the renderer's suppression
deleted, ancestor-suppression.test.ts passes 18/18 while the storm rages.

THE ONE E2E ASSERTED NOTHING. It focused an editor, pressed the chord against an
empty notice stack, and asserted the editor still had focus -- true whether the
binding resolves, is inert, or is deleted outright, and its own comment described
a focused terminal it never created. Replaced by notice-focus-chord.e2e.ts: a
real cmd, a real notice, and an assertion that focus MOVED. focus.notice moves to
COVERED_ELSEWHERE, which the manifest guard verifies by reading the keystroke out
of the code. The budget is unchanged -- one declaration, one @window tag, a
different file.

FOCUS.NOTICE COULD NOT REACH MOST NOTICES. FR-020 says "the most recent notice on
screen", unqualified, but focusMostRecentNotice targeted the most recent notice
CARRYING A CASUALTY LIST. A rename collision, a refused delete, a failed watcher
-- each is a sentence and a Dismiss button, each is commoner than the
consolidated kind, and on each the binding did nothing. Worse, with a plain
notice newest it skipped backwards to an older one that had a list, which FR-020d
forbids in as many words. Now targets the newest notice card and prefers its list
within. FR-025's affordance rule is untouched: being reachable and advertising a
tab stop are different claims.

T063 -- each guard reverted once, run, and observed red; the pairings are in
tasks.md and the PR. Nothing ships from that exercise (FR-030b).

T062a labels the four already-true requirements (FR-008ca, FR-012a, FR-017a,
FR-021a) so an unlabelled one stops reading as a missing implementation.

Two traps recorded where they bite: e2e-budget.test.ts counts categories with a
per-line regex, so a wrapped test( declaration is counted in the total but
belongs to no category (measured: @window 191 against a budget of 192 while the
total was right); and window-chord-manifest.test.ts wants the literal keystroke,
not a named constant.

npm run gate: GREEN, all 8 stages, 24m 25s. lint, typecheck, build, unit 3189,
component 1020, integration 529, contract 138, e2e 205 parallel + 354 serial,
zero retries.
Two things this session paid for, written where the next person hits them.

REPRODUCING A FLAKE: separate invocations, never --repeat-each. The rule already
says stress the one test until it fails on demand; it did not say that HOW you
stress it decides whether the answer means anything. --repeat-each replays a test
inside one worker against the state the previous repeat left, which the suite
never does -- so for a spec about state that persists it manufactures failures.
Measured: with --repeat-each=5, preferences-reset.e2e.ts:265 failed waiting for
binding-reset-zoom.in to become enabled, and the button was disabled because the
PREVIOUS repeat's reset-all was still in effect. A perfect-looking reproduction
of a defect that was not there, and it aimed the investigation at the diff rather
than at the harness. Re-run as separate invocations, that test passed 4 of 4 and
the real flake surfaced somewhere else: theme-flash.e2e.ts:199.

And run the same rounds on origin/master before attributing anything. Branch 1
failure in 4 rounds against master 2 in 6, failing DIFFERENT tests, is what
turned "my branch broke the preferences specs" into issue #341.

THE BUDGET COUNTS CATEGORIES PER LINE. e2e-budget.json gains a note: a test(
declaration wrapped across several lines is counted in the total but its tags are
invisible, so it belongs to no category. Measured: @window read 191 against a
budget of 192 while the total was right, which is a confusing failure to reach
from the other direction.

Docs and one JSON comment only; no test reads either. e2e-budget.test.ts re-run:
5 passed.
@Bidthedog
Bidthedog force-pushed the feature/S041-I278-I314-I327-I328-notice-consolidation branch from b554891 to eb8cbda Compare August 27, 2026 16:01
@Bidthedog
Bidthedog marked this pull request as ready for review August 27, 2026 16:02
@Bidthedog
Bidthedog merged commit 602f4c3 into master Aug 27, 2026
4 checks passed
@Bidthedog
Bidthedog deleted the feature/S041-I278-I314-I327-I328-notice-consolidation branch August 27, 2026 16:11
Bidthedog added a commit that referenced this pull request Aug 27, 2026
T047a (the FR-019 banner guard, 4/4 passing), T064 (no mutation harness or gate stage
added — verified against the diff and package.json), T065 (docs/testing.md needed no edit;
it references e2e-budget.json rather than restating its numbers) and T066 (affected.ts's
header now describes casualties and records why the panel-named symbols keep their names).
74 of 81.

The status block is the point of this commit rather than the four checkboxes. An unticked
task looks the same whether it was forgotten, deferred, or deliberately not done — and
these seven are open for three different reasons, only one of which is ordinary unfinished
work.

Three cannot be reproduced. #278 declares a second defect, a raw ENOENT rendered as the
notice's second line, and the route by which it reaches the screen was not found: ENOENT
classifies at cause.ts:102, so files-service returns the spoken sentence and the raw text
rides in copyDetail, which nothing renders. Nothing was changed on a theory, because a fix
behind no failing test cannot be verified by anything — including the green suite that
would follow it.

One is the developer's: the manual regression watch is not automatable and not the
implementer's to tick.

Three are genuinely outstanding, and T063 is the one with teeth — FR-030a's sensitivity
proof, where each guard's fix is reverted once and that guard is observed to fail. A guard
nobody has watched go red is an assertion that it would, which is exactly the assumption
Group 5 exists to stop making.

NOT regenerated. /speckit-tasks builds a task list from the template, which would have
rewritten this file wholesale and taken 74 ticks, the per-task deviation notes, and the
task IDs that five commit messages and PR #337 reference with it. The need was four
checkboxes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:editor Code editor panels, Monaco, find & replace area:explorer File explorer tree, file operations area:ui-shell Main window, panes, tabs, title bar, notices bug Something isn't working enhancement New feature or request

Projects

None yet

1 participant