Skip to content

fix: the seven views where the box was not the obvious root (#1297, part 3) - #1332

Merged
rbuergi merged 10 commits into
mainfrom
fix/1297-branching-root-views
Aug 12, 2026
Merged

rbuergi merged 10 commits into
mainfrom
fix/1297-branching-root-views

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Part 3 — the last 7 of the 17 both-drop views, plus a resolution for the audit's one "unclear"
entry. With this the 25 views in #1297 are done.

🔗 Stacked on #1327 (base is fix/1297-literal-root-views). Merge order: #1323 → #1327 →
this.
GitHub re-targets automatically as each merges; the diff here is this PR's own 7 views.

Why these were held back for their own PR

These are the ones where "where does the box go" had a real answer that was not the one root the view
already has
. Four of them are not even plain omissions — the value was actively overwritten or
shadowed, which is why the audit flagged them as easy to misread as already-correct.

View The actual defect Decision
MeshNodeCardView card sits inside an <a> that exists only to make it navigable (display:block; width:100%) on the card, not the anchor — applying to both would double a declared margin. In the branch that delegates to an embedded area, forward onto that control (no wrapper)
MeshNodeThumbnailView Class/Style parameters present but hard-coded — the bound values were shadowed, not missed literals stay as defaults, author's tokens join / follow
LayoutAreaDefinitionView fixed class list on the card anchor, no style attribute at all that anchor
NavLink Class="@(IsActive ? "active" : null)" — the active-state token was the whole value, so a declared class was overwritten join both tokens; keep null (not "") when there is nothing to say
DialogView style slot composes --dialog-width / --dialog-height from Size author's declaration after the variables, so a declared width overrides the preset
MeshNodePickerView a FormComponentBase whose root ignored ComputedStyle emit ComputedStyle — see below
MeshNodeRoleEditorView four branch roots, two of them steady-state the editable <div> and the read-only label

MeshNodePickerView — same fix, not a different one

The issue asks whether its extra Width/Height drop is the same fix. It is, and it is one line.
FormComponentBase.ComputedStyle already folds Style + Width + Height into a single string, and
every other form view emits it. This one emitted nothing. Switching its root to style="@ComputedStyle"
recovers all three at once; there is no separate Width/Height mechanism to build.

MeshNodeRoleEditorView — two of four branches, deliberately

Style/Class go on the two steady-state roots (the editable select+checkbox row, and the read-only
label). The loading FluentProgressRing and the <span></span> no-role branch are left alone: the
first is a transient placeholder — styling it and then dropping the style when the real content
arrives is a restructure mid-flight — and the second renders deliberately nothing.

NamedAreaView: resolved as deliberately N/A (bucket ⚪)

The audit could not settle this one from the code. Resolved here, with the reasoning recorded in the
view
so it does not have to be re-derived:

A NamedAreaControl is a reference to an area, not a box. In the steady state the view renders
exactly one thing — <DispatchView> on the resolved RootControl — and that control applies its own
Style/Class to its own element. It belongs with the other pure-indirection views (DispatchView,
EditFormView, PropertyView, ContainerView).

Two reasons a wrapper would be actively wrong rather than merely unnecessary:

  1. Blast radius. Every layout area reaches the DOM through here (LayoutAreaView), as does every
    tab body and dialog body. One extra block box would land around all of them — the blanket-wrapper
    regression fix: HtmlView honours Style/Class instead of silently dropping them #1288 refused, applied to the busiest path in the UI.
  2. The markup it does emit is the not-yet-loaded placeholder (skeleton / spinner). Styling that
    would apply the declaration to a spinner and remove it when content arrives — the visible mid-flight
    restructure that fix: HtmlView honours Style/Class instead of silently dropping them #1288's "gate on the declaration, not the value" rule exists to prevent.

If a real call site ever needs a styled named area, the fix is to forward the declaration onto
RootControl — exactly what MeshNodeCardView now does for its embedded area — not to introduce an
element here.

Tests — verified by falsification

Eight tests added. Reverting the five rendered views:

Failed  MeshNodeCardControl_StyleAndClass_ReachTheCardNotTheAnchor
Failed  MeshNodeThumbnailControl_StyleAndClass_AreNoLongerShadowedByLiterals
Failed  LayoutAreaDefinitionControl_StyleAndClass_ReachTheCardAnchor
Failed  NavLinkControl_Class_JoinsTheActiveStateToken
Failed  DialogControl_StyleAndClass_FollowTheSizeVariables
Failed! - Failed: 5, Passed: 39, Total: 44

Two assertions worth pointing at:

  • MeshNodeCardControl_…_ReachTheCardNotTheAnchor asserts the anchor does not also carry the
    declaration — the double-margin trap, which a Contain-only test would have passed.
  • NavLinkControl_WithoutClass_EmitsNoClassAttribute pins that the joined value stays null rather
    than "", so an inactive unstyled nav link emits no class attribute exactly as before.

Two of the seven are not rendered, stated in the test class doc: MeshNodePickerView needs a live
pointer stream (as every form view does), and MeshNodeRoleEditorView renders a progress ring until
its role loads.

Verification

Check Result
dotnet build src/MeshWeaver.Blazor -c Release -warnaserror 0 Warning(s), 0 Error(s)
dotnet build test/MeshWeaver.Hosting.Blazor.Test -c Release -warnaserror 0 Warning(s), 0 Error(s)
ControlStyleRenderingTest (44) Passed
WhatsNewEntryIntegrityTest + DocumentationLinkIntegrityTest Passed

What's New entry added (Category: Fix).

What remains of #1297 after this

Only the three views that were outside the audit's buckets: SearchBoxView, MeshSearchView and
Monaco/CodeEditorView are registered as control views but derive from plain ComponentBase, so
Style/Class are never bound — there is nothing being dropped in markup, because nothing arrives.
That is a different defect (a view-base question, not a markup one) and wants its own issue rather than
a hurried [Parameter] pair here.

🤖 Generated with Claude Code

rbuergi and others added 3 commits August 12, 2026 17:50
Eight control views write Style="@Style" onto a real root element of their own and
never wrote the author's Class beside it. Unlike the wrapper cases in #1288 there is
no "where does the box go" question here: the element already carrying the Style is
by definition the element that must carry the Class.

Two shapes, chosen per view by what its root already has:
  * root with NO class of its own  -> class="@Class" (Blazor omits a null attribute)
  * root with a FIXED class list   -> "fixed@(ClassSuffix)"

BlazorView.ClassSuffix renders " " + Class, or the empty string when nothing was
declared, so an unstyled control's attribute stays byte-identical. Appending " @Class"
literally would leave a trailing space in every such attribute on every page.

LayoutAreaView and ThreadMessageBubbleView compose their class in code instead: Razor
cannot parse two adjacent explicit expressions in one attribute value, and both roots
end in a conditional expression.

Nothing here is gated on the declaration (ViewModel.Class) as HtmlView is, because no
element is created or removed — a JsonPointerReference that lands a frame later just
updates an attribute; it cannot restructure the DOM mid-flight.

Tests: five render through the real HtmlRenderer and were verified by falsification —
reverting each view makes exactly its own test fail (5 failed / 17 passed). The four
backwards-compatibility guards pass with AND without the fix, asserting the exact
attribute (class="markdown-body", not Contain) so a stray trailing space or empty
class="" fails. NumberFieldView, LayoutAreaView and ThreadMessageBubbleView are fixed
the same way but need a live pointer/area/thread stream to render; that is stated in
the test class doc rather than left implicit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ten control views render a panel / page / editor with ONE root element of their own,
carrying only hard-coded declarations (`style="max-width: 720px;"`,
`class="node-export-view"`) and no author input at all. That root IS the control's
box, so it is where the author's Style and Class belong — no wrapper is introduced
anywhere here, which is the point: #1288 showed a blanket wrapper breaks call sites
that depend on a fragment's own root being the direct flex child.

Placement rule, applied per view rather than mechanically:
  * the author's Style goes LAST (BlazorView.StyleSuffix) so a declared width/height
    OVERRIDES the view's default. Dropping it was the bug; losing the cascade would
    be the same bug with extra steps.
  * the author's Class joins the fixed class list (ClassSuffix), never replaces it.

Three compose in code because Razor cannot parse two adjacent explicit expressions in
one attribute value: CatalogView (its root style was already a code-behind literal),
CollaborativeMarkdownView, and DiffEditorView — whose host box read ViewModel.Height
directly and now keeps it as the default with the declared Style after it.

Backwards compatibility: with nothing declared both suffixes are empty, so each root
is byte-identical — asserted as exact attributes, not Contain. One deliberate
exception: CollaborativeMarkdownView's container previously emitted a trailing space
when its view-mode class was empty (the default mode); the tokens are now joined, so
that space is gone. A class attribute is a whitespace-separated token list, so it
never participated in a selector match.

Tests: nine render through the real HtmlRenderer, verified by falsification —
reverting the views fails exactly those nine (9 failed / 26 passed) while the four
backwards-compatibility guards pass either way. MeshNodeContentEditorView is fixed
the same way but not rendered: its first render is a progress ring and the styled
root appears only after the node's fields load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last of the both-drop bucket, and the only ones where the answer was NOT "the one
root the view already has". Each decided on its own:

* MeshNodeCardView renders the card inside an <a> that exists only to make it
  navigable (display:block; width:100% — a transparent pass-through). The CARD is the
  box the user sees and sizes, so the declaration goes there; putting it on the anchor
  as well would apply a declared margin twice. In the branch where the card delegates
  to an embedded LayoutArea it renders no element of its own, so the declaration is
  FORWARDED onto that control — the shape PropertyView/EditFormView already use — and
  only when actually declared, so an undeclared card yields the identical record.
* MeshNodeThumbnailView and LayoutAreaDefinitionView had literals SHADOWING the bound
  values rather than merely missing them, which is why they read as already-correct.
* NavLink's Class was not forgotten but OVERWRITTEN: Class="@(IsActive ? "active" :
  null)" made the active-state token the whole value. Both tokens now join, and the
  result stays null (not "") so an inactive unstyled link still emits no attribute.
* DialogView's style slot composes --dialog-width/--dialog-height from Size; the
  author's declaration follows them so a declared width overrides the preset.
* MeshNodePickerView is a FormComponentBase whose root ignored ComputedStyle, so it
  dropped Width and Height along with Style. Emitting ComputedStyle is the same
  one-line fix, not a separate one.
* MeshNodeRoleEditorView applies to its two steady-state branch roots (editable and
  read-only). The progress ring and the deliberately-empty span are left alone.

NamedAreaView — the audit's one "unclear" — is resolved as deliberately N/A, with the
reasoning recorded in the view: a NamedAreaControl is a REFERENCE to an area, not a
box, and every layout area, tab and dialog body reaches the DOM through it, so a
wrapper here is the blanket-wrapper regression #1288 refused, applied to the busiest
path in the UI. If a call site ever needs it, the fix is to forward onto RootControl.

Tests: five render through the real HtmlRenderer, verified by falsification (5 failed
/ 39 passed with the views reverted). MeshNodePickerView needs a live pointer stream
and MeshNodeRoleEditorView renders a progress ring until its role loads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi

rbuergi commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Re-targeted to main: CI (dotnet-test.yml) only triggers on pull_request: branches: [main], so with the stacked base this PR would have carried no checks at all. The branch contains #1323 and #1327 (it needs the ClassSuffix/StyleSuffix helpers), so the diff shows all three until they merge. Merge order: #1323 → #1327 → this.

rbuergi and others added 4 commits August 12, 2026 18:20
The NotContain needle was missing the space StyleSuffix prefixes, so the assertion
matched nothing whether or not the anchor double-applied the style — a guard that
cannot fail is not a guard. Anchored on "color: inherit", which appears only in the
anchor's style (the card's own literal also ends in "box-sizing: border-box;", so a
shorter needle would match the CARD and pass regardless).

Verified by temporarily appending StyleSuffix to the anchor: this assertion fails, the
two positive assertions in the same test still pass. 44/44 with the change reverted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 12, 2026 16:29
…oc example

Two of Copilot's three findings on #1323, both real:

* LayoutAreaView produced a DOUBLE space (and an empty class token) when the area is
  neither Top nor Fill and a class was declared — the interpolated literal already
  ended in a space before the empty conditional, and ClassSuffix adds its own. Joined
  as a token list instead, which also drops the pre-existing trailing space in that
  case. Same treatment CollaborativeMarkdownView gets in part 2, same justification: a
  class attribute is a whitespace-separated token list, so neither the old space nor
  its removal can change a selector match.
* The ClassSuffix XML doc used 'markdown-body@ClassSuffix' as its example — the exact
  bare form that Razor's email-address heuristic emits as LITERAL TEXT with no error.
  Copying the doc would have reintroduced the bug; it now shows @(ClassSuffix) and
  says why.

The third finding (that Contain("class=\"markdown-body\"") would still pass on a
trailing space) does not hold: the needle includes the CLOSING QUOTE, so
class="markdown-body " and class="markdown-body x" both fail to match. Answered on
the thread rather than changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR continues the #1297 sweep to ensure UiControl.Style / UiControl.Class are consistently reflected in emitted Blazor markup (or deliberately documented as N/A), backed by renderer-based regression tests and “What’s New” entries.

Changes:

  • Introduces StyleSuffix / ClassSuffix helpers on BlazorView to safely append author styling without changing unstyled markup.
  • Updates a broad set of control views to forward/join Style and Class to the correct rendered element (including “which element?” cases, shadowed literals, and active-state class joining).
  • Expands ControlStyleRenderingTest to render and assert emitted HTML for the updated views, plus adds “What’s New” documentation entries.

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/MeshWeaver.Hosting.Blazor.Test/ControlStyleRenderingTest.cs Extends renderer-based tests to assert Style/Class reach the correct DOM element and preserve unstyled markup byte-for-byte.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-13-styling-hooks-now-reach-eight-more-parts-of-the-page.md “What’s New” entry describing the class-hook fixes for additional controls.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-13-panels-and-editors-now-accept-the-styling-you-give-them.md “What’s New” entry describing fixes for literal-root panels/editors that previously ignored styling.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-13-cards-links-and-dialogs-honour-their-styling-too.md “What’s New” entry describing fixes for the “which element?” styling cases and form width/height pickup.
src/MeshWeaver.Blazor/Components/ThreadMessageBubbleView.razor.cs Composes a bubble class string that preserves role classes and appends author Class.
src/MeshWeaver.Blazor/Components/ThreadMessageBubbleView.razor Applies composed class so author Class reaches the message bubble container.
src/MeshWeaver.Blazor/Components/TabsView.razor Forwards Class into FluentTabs.
src/MeshWeaver.Blazor/Components/NumberFieldView.razor Applies author Class on the wrapper that already owns the computed style/size.
src/MeshWeaver.Blazor/Components/NodeImportView.razor Appends Style/Class to the literal root without changing unstyled output.
src/MeshWeaver.Blazor/Components/NodeExportView.razor Appends Style/Class to the literal root without changing unstyled output.
src/MeshWeaver.Blazor/Components/NavLink.razor Joins active-state token with author Class and forwards Style to link variants.
src/MeshWeaver.Blazor/Components/NamedAreaView.razor Documents why NamedAreaView is deliberately N/A for applying its own Style/Class.
src/MeshWeaver.Blazor/Components/Monaco/DiffEditorView.razor Moves host sizing to a composed style string and applies author Class/Style to host box.
src/MeshWeaver.Blazor/Components/MeshNodeThumbnailView.razor.cs Avoids shadowing by composing FluentCard class/style to include author tokens.
src/MeshWeaver.Blazor/Components/MeshNodeThumbnailView.razor Uses composed card class/style so author styling is honored.
src/MeshWeaver.Blazor/Components/MeshNodeRoleEditorView.razor Applies author Style/Class to the steady-state editable and read-only roots.
src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor.cs Composes picker class to include layout state plus author Class.
src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor Emits ComputedStyle on the root so Style/Width/Height are honored; applies composed class.
src/MeshWeaver.Blazor/Components/MeshNodeContentEditorView.razor Appends author Style/Class to the editor root when it renders.
src/MeshWeaver.Blazor/Components/MeshNodeCollectionView.razor Appends author Class to the collection container’s fixed class list.
src/MeshWeaver.Blazor/Components/MeshNodeCardView.razor Applies author styling to the visible card (not the navigation anchor) and forwards styling onto embedded-area control when used.
src/MeshWeaver.Blazor/Components/MarkdownView.razor Appends author Class to the fixed markdown-body class list.
src/MeshWeaver.Blazor/Components/MarkdownEditorView.razor Adds Style/Class support to the editor container.
src/MeshWeaver.Blazor/Components/LayoutAreaView.razor Composes container class to preserve existing state tokens and append author Class.
src/MeshWeaver.Blazor/Components/LayoutAreaDefinitionView.razor Applies author Style/Class to the card anchor root.
src/MeshWeaver.Blazor/Components/Label.razor Forwards Class into FluentLabel for both clickable and non-clickable branches.
src/MeshWeaver.Blazor/Components/ExportDocumentView.razor Appends author Style/Class to the literal root without changing unstyled output.
src/MeshWeaver.Blazor/Components/EditorView.razor Applies author Class to the wrapper that already owns Style.
src/MeshWeaver.Blazor/Components/DialogView.razor.cs Ensures author Style is appended after size-derived CSS variables.
src/MeshWeaver.Blazor/Components/DialogView.razor Forwards Class into FluentDialog and uses the composed style.
src/MeshWeaver.Blazor/Components/CommentableView.razor Applies author Style/Class to the wrapper positioning context element.
src/MeshWeaver.Blazor/Components/CollaborativeMarkdownView.razor.cs Composes container class token list to avoid shadowing/trailing-space artifacts and include author Class.
src/MeshWeaver.Blazor/Components/CollaborativeMarkdownView.razor Applies composed class and forwards Style to the collaborative markdown container.
src/MeshWeaver.Blazor/Components/CatalogView.razor.cs Appends author Style after container defaults so author values win the cascade.
src/MeshWeaver.Blazor/Components/CatalogView.razor Appends author Class on the catalog container.
src/MeshWeaver.Blazor/Components/AppearanceView.razor Appends author Style after the default max-width and forwards Class.
src/MeshWeaver.Blazor/BlazorView.razor.cs Adds StyleSuffix and ClassSuffix helpers to preserve unstyled markup and ensure author styling wins the cascade when appended.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

722 tests  ±0   722 ✅ ±0   15m 32s ⏱️ + 1m 0s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 8ae6100. ± Comparison against base commit 408ed73.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   8m 23s ⏱️ +24s
1 814 tests ±0  1 711 ✅ ±0  103 💤 ±0  0 ❌ ±0 
2 074 runs  +3  1 971 ✅ +3  103 💤 ±0  0 ❌ ±0 

Results for commit 8ae6100. ± Comparison against base commit 408ed73.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 885 tests  ±0   1 693 ✅ ±0   8m 33s ⏱️ -4s
   11 suites ±0     192 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 8ae6100. ± Comparison against base commit 408ed73.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 847 tests  ±0   1 837 ✅ ±0   12m 32s ⏱️ -25s
   11 suites ±0      10 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 8ae6100. ± Comparison against base commit 408ed73.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

2 380 tests  +31   2 379 ✅ +31   8m 9s ⏱️ -11s
   12 suites ± 0       1 💤 ± 0 
   12 files   ± 0       0 ❌ ± 0 

Results for commit 8ae6100. ± Comparison against base commit 408ed73.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 372 tests  ±0   1 372 ✅ ±0   7m 16s ⏱️ +20s
   11 suites ±0       0 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 8ae6100. ± Comparison against base commit 408ed73.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

    66 files  ± 0     66 suites  ±0   1h 0m 28s ⏱️ + 1m 5s
10 020 tests +31  9 714 ✅ +31  306 💤 ±0  0 ❌ ±0 
10 280 runs  +34  9 974 ✅ +34  306 💤 ±0  0 ❌ ±0 

Results for commit 8ae6100. ± Comparison against base commit 408ed73.

@rbuergi
rbuergi merged commit c392884 into main Aug 12, 2026
19 checks passed
rbuergi added a commit that referenced this pull request Aug 12, 2026
…ads" (#1314 probes)

Unblocks this PR's CI. The failing test —
`LogIncidentIngestAccessContextTest.FirstSighting_IsAttributedToSystem_NotTheEmissionThreadsAmbient`
(shard 4) — is NOT caused by the disposal-join change here. It is a pre-existing flake on main:
the identical failure is recorded on main commit `c39288499` (merge of #1332, run 31634660324),
which predates this PR's CI run, and the test file (`cb40296af`, #1314) is not touched by this
diff.

## What actually failed

    Did not expect value to be 4 because the absent-node read completes off the subscribing
    thread …

i.e. `hop.SelectorThread.Should().NotBe(hop.SubscribeThread)`, on a run whose own probe output
shows the pipeline behaved perfectly:

    [create/cold-absent-node] subscribeThread=4@0.2ms selectorThread=4@6.2ms

The stage deferred by six milliseconds and the thread pool handed the continuation back to the
same thread it was subscribed from. Nothing was wrong; the assertion measures the wrong thing.

## The assertion was wrong twice over, so this is not merely "make it green"

1. **Wrong property.** A deferred continuation routinely lands on the subscribing thread, so
   thread identity cannot distinguish "ran inline" from "ran later". The file's own negative
   control already records the other half of this — *"an `AsyncLocal` DOES reach a pool hop
   scheduled from inside the scope … 'different thread' alone never proved a lost identity"* —
   so a thread change was never evidence about the identity either.
2. **Wrong probe.** It guarded `hop`, which makes no identity assertion at all. The identity
   assertion is on `hopNoFlow`, and that one had no non-vacuity guard: a stage running inside the
   still-open scope would see `system-security` for free, and the assertion would have proved
   nothing about `CarryAccessContext` carrying it.

## The replacement is strictly stronger

`InlineDetector` measures the property the probes actually need — *the stage was not run
synchronously nested inside the Subscribe call* — and measures it EXACTLY rather than by proxy.
`Enter`/`Exit` bracket the Subscribe call on the subscribing thread, and `Observe` reports true
only for a stage on that same thread while the call is still on the stack. Same thread ⇒ ordered
by program order; any other thread ⇒ not nested by construction. No interleaving gives a wrong
answer, so it cannot flake the way the thread comparison did.

- The guard now sits on **`hopNoFlow`** in the create path, where the identity assertion is, and a
  matching guard is added to the fold path's `hop`, which had none.
- **`ProbeInlineDetectorSeesInline` is a POSITIVE CONTROL** for the detector, and is the reason
  this is a pin rather than an assertion that cannot fail: a "did not happen" assertion is
  worthless unless the detector demonstrably reports the thing when it does happen. It runs the
  same measurement path with nothing between Subscribe and the selector, and must report inline.
- The negative control's own "it must actually defer" check moves off thread identity too — same
  latent flake, one line from firing.

## Mutation-tested

Replacing the framework read in `ProbeScopeReach` with an inline `Observable.Return` turns BOTH
new guards red with their intended messages (`ranInline=True` on the real probes, positive control
still `True`). Reverted; green again.

Suites: `MeshWeaver.Observability.Test` 124/124, `MeshWeaver.Messaging.Hub.Test` 214/214 (this
PR's own change area, including `HostedHubDisposalJoinTest` and `InflightHubCreationDrainTest`).
Clean under `-c Release -warnaserror --no-incremental`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants