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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ Conventions for contributors:

### Added

- **`REACTOR_DYM_006`: color strings that render gray** (issue #1375, spec 061 §7.3). A new
default-on **Warning** flags compile-time-constant color strings that `BrushHelper` can't
parse and therefore silently fall back to gray: `.Background("orange")`, `.Foreground("#888")`,
or a font name passed to `ResourceBuilder.Set(key, string)` under a `*FontFamily` key. It covers
`BrushHelper.Parse`, `ResourceBuilder.Set(string, string)` and the `string` overloads of
`Background` / `Foreground` / `BorderBrush` / `WithBorder`, suggests the exact hex (or
`new FontFamily(...)` once that `Set` overload exists), and ships a code fix; `mur check`'s new
`ColorStringLiteralRule` repeats the rewrite as a `→ try:` hint. Unlike `REACTOR_DYM_001`–`005`
it fires on code that compiles, so a project with `TreatWarningsAsErrors` can stop building on
an existing gray color. Fix the value, or adjust the severity with
`dotnet_diagnostic.REACTOR_DYM_006.severity` in `.editorconfig`.

- **Inspector diagnostics: call-site column and a per-component render event**
(issue #1326, spec 010 §1.1):
- `SourceLocation.ColumnNumber` (1-based, `0` = unknown), a third positional
Expand Down
1 change: 1 addition & 0 deletions docs/_pipeline/templates/analyzer-architecture.md.dt
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ sibling `src/Reactor.Analyzers.Internal/` project instead; see
| `REACTOR_DYM_003` | Warning | Mistyped Reactor factory name in call position (e.g. `Buton(...)`) | `FuzzyFactoryNameAnalyzer.cs` |
| `REACTOR_DYM_004` | Warning | Reactor factory called with too few arguments (CS7036), single unique overload — suggests the parameter shape | `MissingFactoryArgumentAnalyzer.cs` |
| `REACTOR_DYM_005` | Warning | String passed where a Reactor `Element` is expected (CS1503) — wrap it in a text factory | `StringForElementArgumentAnalyzer.cs` |
| `REACTOR_DYM_006` | Warning | Constant color string `BrushHelper` can't parse, so it renders gray (e.g. `.Background("orange")`, `.Foreground("#888")`, a font name under a `*FontFamily` resource key) — suggests the hex value (or `new FontFamily(...)` once that `ResourceBuilder.Set` overload exists) | `ColorStringLiteralAnalyzer.cs` |

`REACTOR_HOOKS_002` and `_003` were the reserved control-flow / data-flow slots
(variable hook counts across early returns, async boundaries inside `UseEffect`);
Expand Down
1 change: 1 addition & 0 deletions docs/guide/analyzer-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ sibling `src/Reactor.Analyzers.Internal/` project instead; see
| `REACTOR_DYM_003` | Warning | Mistyped Reactor factory name in call position (e.g. `Buton(...)`) | `FuzzyFactoryNameAnalyzer.cs` |
| `REACTOR_DYM_004` | Warning | Reactor factory called with too few arguments (CS7036), single unique overload — suggests the parameter shape | `MissingFactoryArgumentAnalyzer.cs` |
| `REACTOR_DYM_005` | Warning | String passed where a Reactor `Element` is expected (CS1503) — wrap it in a text factory | `StringForElementArgumentAnalyzer.cs` |
| `REACTOR_DYM_006` | Warning | Constant color string `BrushHelper` can't parse, so it renders gray (e.g. `.Background("orange")`, `.Foreground("#888")`, a font name under a `*FontFamily` resource key) — suggests the hex value (or `new FontFamily(...)` once that `ResourceBuilder.Set` overload exists) | `ColorStringLiteralAnalyzer.cs` |

`REACTOR_HOOKS_002` and `_003` were the reserved control-flow / data-flow slots
(variable hook counts across early returns, async boundaries inside `UseEffect`);
Expand Down
9 changes: 5 additions & 4 deletions docs/reference/mur-check-did-you-mean.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ mur check <path>
│ fuzzy-match against the real Reactor API surface.
│
│ Tier 3 — induced + vocabulary rules (SHIPPED in part, Phase 3)
│ Small symbol-bound rewriters. Six rules live today
│ (four Class-A induced + two Class-B vocabulary).
│ Small symbol-bound rewriters. Seven rules live today
│ (4 Class-A + 2 Class-B + 1 analyzer mirror).
│ Rules can cover diagnostic codes Tier-2 doesn't
│ (e.g. CS1955). Rules WIN over Tier-2 when both match.
│
Expand Down Expand Up @@ -168,7 +168,7 @@ Each rule is a small file under `src/Reactor.Cli/Check/Rules/`, registered into

Each rule passes a six-bar **Validation Gate** before merge (frequency, cross-agent reproducibility, positive fixtures, negative counter-examples, independent reviewer signoff, kill-switch). The gate exists because a bad rule is worse than no rule — it contaminates every downstream agent session. Class-B (vocabulary-translation) rules waive bar #1 (frequency) — their justification is the documented prior-framework citation rather than a corpus cluster. The audit at `docs/specs/tasks/038-tuning-reports/2026-05-11-cross-agent-audit.md` is where bar #2 (cross-agent) gets formally cleared per cluster.

**Six rules shipped to date** (four Class-A induced + two Class-B vocabulary):
**Seven rules shipped to date** (four Class-A induced + two Class-B vocabulary + one analyzer mirror):

| Rule | Class | Code(s) | Receiver | What it suggests | Cross-agent events |
|---|---|---|---|---|---|
Expand All @@ -178,6 +178,7 @@ Each rule passes a six-bar **Validation Gate** before merge (frequency, cross-ag
| `GridSizePxRenameRule` | A | CS0117 | `GridSize` | `GridSize.Px(...)` for `Pixel`/`Pixels`/`Fixed` legacy names | 9 |
| `TextBlockStyleHintRule` | A | CS1061 / CS0117 | `TextBlockElement` | fluent text helpers (`.FontSize`, `.SemiBold`, …); Reactor has no `Style` member | 5 across both `.Style(...)` and `with { Style = ... }` shapes |
| `ThemeRawResourceKeyRule` | A | CS0117 | `Theme` | real `Theme.*` aliases (`Theme.LayerFill`, `Theme.Accent`, …) for WinUI `<X>FillColor<Y>` ThemeResource key names | run5 opus-4.7 only; bar #2 partial |
| `ColorStringLiteralRule` | analyzer mirror | `REACTOR_DYM_006` | `BrushHelper` / `ResourceBuilder` / `ElementExtensions` | the hex for an unparseable constant color string (`"orange"` → `"#FFA500"`, `"#888"` → `"#888888"`), or `new FontFamily("…")` under a `*FontFamily` resource key when that `ResourceBuilder.Set` overload exists. **First rule anchored on an analyzer ID**: the mistake compiles, so there is no CS code to hang it on (issue #1375); the rewrite is read from the analyzer's message | n/a — mirrors the in-build analyzer |

### Suggest-gate carve-out for Tier-3 rules

Expand Down Expand Up @@ -503,7 +504,7 @@ Branch: `eval/spec-038-ec3-2026-05-11` at commit `2b7090f`. Six rules + the two
- `RuleSymbolResolver.cs` — `ResolveType(string)`, `ResolveMethod(INamedTypeSymbol, string)`, `ResolveMember(INamedTypeSymbol, string)`. Cached via `ConditionalWeakTable<CSharpCompilation, _>` so two callers with the same compilation reference share caches; per-compilation resolver identity is test-locked.
- CLI: `--disable-rule <Name>` (repeatable), `--list-rules` (prints name/provenance/status table; short-circuits before `dotnet build` runs). Unknown `--disable-rule` names warn instead of error.

**The six rules** — see the table in §3 for the full inventory. Four Class-A (induced from the cross-corpus audit) + two Class-B (vocabulary-translation, structurally justified). All six bind through `RuleSymbolResolver`; no string-matching.
**The six rules** — see the table in §3 for the full inventory. Four Class-A (induced from the cross-corpus audit) + two Class-B (vocabulary-translation, structurally justified). All six bind through `RuleSymbolResolver`; no string-matching. A seventh, `ColorStringLiteralRule` (issue #1375), landed later on `main`: it mirrors the in-build `REACTOR_DYM_006` analyzer rather than a corpus cluster, anchors on that analyzer ID, and reads its rewrite from the analyzer's message.

**Two critical correctness fixes** (both blocked any real-world rule firing prior; surfaced by end-to-end smoke testing during Phase 3):

Expand Down
67 changes: 63 additions & 4 deletions docs/specs/061-in-build-did-you-mean.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
**Implemented (in part) — 2026-07-07.** Phase 1 (`REACTOR_DYM_001`) and Phase 2 (`REACTOR_DYM_002`)
have shipped, along with the first fuzzy analyzer (`REACTOR_DYM_003`, the CS0103
mistyped-factory-name case) and the argument-shape analyzers (`REACTOR_DYM_004`, CS7036 missing
argument; `REACTOR_DYM_005`, the CS1503 string-for-Element case — see §7.2); the remaining fuzzy
member-resolution phases and the shared match-engine extraction remain design-only. This spec
argument; `REACTOR_DYM_005`, the CS1503 string-for-Element case — see §7.2), and the first did-you-mean
that rides **no** compiler error (`REACTOR_DYM_006`, unparseable color-string literals — see §7.3); the
remaining fuzzy member-resolution phases and the shared match-engine extraction remain design-only. This spec
is the design of record for bringing Reactor's "did you mean" suggestions — today reachable only
through the `mur check` CLI — into a plain `dotnet build` and the IDE, for consumers of the
`Microsoft.UI.Reactor` NuGet package.
Expand Down Expand Up @@ -321,14 +322,72 @@ produces uncompilable code is worse than none, so neither ships one.
and the IDE. Tunable via `.editorconfig`. The shared gating lives in one `ArgumentShapeGate` helper so the
two analyzers can't drift.

### §7.3 `REACTOR_DYM_006` — unparseable color-string literal (shipped)

Issue #1375. Every Reactor API that takes a color *string* goes through `BrushHelper.ParseColor`, which
knows eight names (`red`, `green`, `blue`, `white`, `black`, `gray`/`grey`, `lightgray`/`lightgrey`,
`transparent`) plus `#RRGGBB` / `#AARRGGBB`. Anything else silently becomes a **gray** brush. So
`.Background("orange")`, `.Foreground("#888")`, and — once `ResourceBuilder.Set(string, FontFamily)` exists
(PR #1370) — `.Resources(r => r.Set("ContentControlThemeFontFamily", "Consolas"))` all compile and render
wrong. `ColorStringLiteralAnalyzer` + `ColorStringLiteralCodeFix` catch the constant cases.

**The first DYM that rides no compiler error.** DYM_001–005 always co-occur with a CS error, which is what
made Warning safe under `TreatWarningsAsErrors`. This one fires on code that compiles. It is still a
**Warning**: the result is always a wrong brush, never a style preference (the same reasoning as
`REACTOR_MOD_003`). In-repo hits were fixed in the PR that introduced it rather than suppressed.

- **Targets (symbol identity, resolved once per compilation).** `BrushHelper.Parse(string)`,
`ResourceBuilder.Set(string key, string color)`, and the `string color` overloads of `Background` /
`Foreground` / `BorderBrush` / `WithBorder` on `ElementExtensions` (generic and RichText). A look-alike API
in another namespace never matches. `D3Charts.Brush(string)` is out of scope: it parses with `D3Color`.
- **Values.** Only compile-time constants: literals, `const`s, constant concatenation, and each constant
branch of a `?:` / `??` / switch expression. A wholly-constant expression is checked as its single value,
and the walk also prunes branches the compiler doesn't fold but a constant rules out (a `const bool`
condition, a constant non-null `??` left side, a constant-valued switch over constant patterns), so a dead
branch is never reported. Runtime strings are out of reach; a runtime throw was considered and
left as a separate decision (it would change behaviour for apps relying on the fallback).
- **Grammar parity.** The analyzer can't reference `src/Reactor`, so `ColorStringClassifier` carries a
netstandard2.0 copy of the grammar, including its quirks (`TrimStart('#')` accepts `##FF0000`;
`NumberStyles.HexNumber` tolerates whitespace inside a byte pair). `BrushHelper` exposes an internal
`TryParseColor` (no behaviour change) so `ColorStringParityTests` can compare exactly — a plain
"result is gray" oracle can't tell an unparseable string from a legitimate `"gray"`.
- **Did-you-mean, in priority order.** (1) a `ResourceBuilder.Set` key ending in `FontFamily` suggests
`new FontFamily("…")`, but **only when** `Set(string, FontFamily)` is present in the compilation and the
value is the whole argument (it can't replace one branch of a `?:`); otherwise the message says the overload
only stores color brushes, points at the `.FontFamily("…")` element modifier, and offers no fix. (2) `#RGB` / `#ARGB`
shorthand → expanded `#RRGGBB` / `#AARRGGBB`. (3) six or eight hex digits without `#` → add it. (4) other
`#…` → "must be `#RRGGBB` or `#AARRGGBB`". (5) a standard CSS / WinUI named color (139 entries, including
the `grey` spellings) → its exact hex, cross-checked against `System.Drawing.KnownColor`. (6) anything else
→ the supported list, hex, or a `Theme` token.
- **Fix.** Cases 1–3 and 5 carry the replacement in the property bag. Literal fixes rewrite the string
literal (including one inside a `?:` branch); the FontFamily fix wraps the whole argument (a literal or a
`const` reference) in a `Simplifier`-annotated `new FontFamily(...)`, renaming a named `color:` argument to
the FontFamily overload's parameter so the call still binds.

**`mur check` mirror.** `ColorStringLiteralRule` is the first Tier-3 rule anchored on an **analyzer** ID
rather than a CS code — there is no compiler diagnostic to augment. `mur check` wraps `dotnet build`, which
runs the analyzer bundle, so `REACTOR_DYM_006` appears in the parsed output. Every fixable message ends in
`did you mean X?`, computed for the exact value the diagnostic points at, so the rule lifts `X` out of the
message rather than re-deriving it. It has no grammar of its own and can't disagree with the code fix, and
the `→ try:` suffix keeps the rewrite visible after `mur check` truncates the message. A `*FontFamily`
key with no rewrite surfaces the message's `.FontFamily("…")` modifier advice the same way; messages with no
concrete next step (unknown name, malformed hex) stay silent. There is deliberately no Tier-1 `HintFor` entry, which would mask the suggestion.
`DidYouMeanAnalyzerParityTests` feeds real analyzer diagnostics to the rule and asserts it suggests the
code fix's rewrite; `ColorStringParityTests` pins the `did you mean` suffix.

**Overlap with `REACTOR_THEME_001`.** THEME_001 already flags every string literal passed to `.Background` /
`.Foreground` / `.WithBorder` as "use a Theme token". That is a style nudge (and is downgraded to
`suggestion` across `samples/`). DYM_006 is the correctness subset: the literal doesn't just ignore the
theme, it renders gray. Both can fire on one literal.

## §8 Risks and guardrails

| Risk | Guardrail |
|---|---|
| False positives under cascading / partial-edit errors | Bail when the receiver type is unresolved or an error type; only fire when the invocation didn't bind; gate to Reactor symbols; (fuzzy phases) require high similarity thresholds. |
| IDE typing latency | Use the provided `context.SemanticModel` (never `Compilation.GetSemanticModel`, RS1030); narrow syntax-node actions; cheap symbol pre-checks before any fuzzy match. |
| Divergence between analyzer and `mur check` | Phase 2: local maps locked to the CLI Tier-3 rules by a cross-check parity test (`DidYouMeanAnalyzerParityTests`). Argument-shape phase (§7.2): the same test asserts `REACTOR_DYM_004`/`_005` and the CLI `SymbolSuggester` both fire on the representative CS7036 / CS1503 shapes. Fuzzy phases: one shared match library + shared fixtures (§7 item 5). |
| Noise (a second diagnostic atop the CS error) | Concise message; `REACTOR_DYM_*` only ever accompanies a genuine compile error; consumers can tune severity via `.editorconfig`. |
| Divergence between analyzer and `mur check` | Phase 2: local maps locked to the CLI Tier-3 rules by a cross-check parity test (`DidYouMeanAnalyzerParityTests`). Argument-shape phase (§7.2): the same test asserts `REACTOR_DYM_004`/`_005` and the CLI `SymbolSuggester` both fire on the representative CS7036 / CS1503 shapes. `REACTOR_DYM_006` (§7.3): `ColorStringParityTests` locks `BrushHelper.TryParseColor` and the analyzer's classifier together; the rule reads the analyzer's own message, and a live-assembly test pins the analyzer's targets to the real Reactor surface. Fuzzy phases: one shared match library + shared fixtures (§7 item 5). |
| Noise (a second diagnostic atop the CS error) | Concise message; `REACTOR_DYM_001`–`_005` only ever accompany a genuine compile error; consumers can tune severity via `.editorconfig`. `REACTOR_DYM_006` is the exception (§7.3) and fires only on a value that provably renders gray. |

## §9 Relationship to `mur check`

Expand Down
1 change: 1 addition & 0 deletions plugins/reactor/skills/reactor-build-and-check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ Additional flags:
| `REACTOR_DYM_003` | warning | An unresolved bare call closely matches a Reactor factory name (e.g. `Buton(...)`) — pairs with compiler `CS0103` | Rename to the suggested factory: `Button(...)`. Fires only on a close, unambiguous factory match (the typo flavour of `CS0103`); the IDE offers a one-click rename. |
| `REACTOR_DYM_004` | warning | A Reactor factory is called with too few arguments (e.g. `ScrollViewer()`) — pairs with compiler `CS7036` | Supply the missing argument(s); the message lists the factory's full parameter shape as named arguments. Fires only when a single overload uniquely matches, so multi-overload factories (e.g. `Button()`) are deliberately left to the raw compiler error. Message only — no code fix (the `<Element>` placeholder wouldn't compile). |
| `REACTOR_DYM_005` | warning | A `string` is passed where a Reactor `Element` is expected (e.g. `ScrollViewer("hi")`) — pairs with compiler `CS1503` | Wrap the string in a text factory: `ScrollViewer(TextBlock("hi"))` (or `Heading`/`Caption`). Narrow, high-confidence special case only; general type mismatches degrade to the raw `CS1503`. Message only — no code fix. |
| `REACTOR_DYM_006` | warning | A constant color string `BrushHelper` can't parse (e.g. `.Background("orange")`, `.Foreground("#888")`, `.Resources(r => r.Set("ContentControlThemeFontFamily", "Consolas"))`). It compiles and silently renders **gray**. | Color strings accept only `red`, `green`, `blue`, `white`, `black`, `gray`/`grey`, `lightgray`/`lightgrey`, `transparent`, `#RRGGBB` and `#AARRGGBB`. Use the suggested hex (`"orange"` → `"#FFA500"`, `"#888"` → `"#888888"`, `"FF0000"` → `"#FF0000"`) or a `Theme` token; the IDE fix and `mur check`'s `→ try:` apply the same rewrite. A font name under a `*FontFamily` resource key is never a color: set the font with the `.FontFamily("…")` modifier on the element instead. |
| `CS0103` | error | "The name 'X' does not exist in the current context" | Missing `using` — most often `Microsoft.UI.Reactor.Layout` (FlexAlign), `Microsoft.UI.Xaml.Controls` (InfoBarSeverity, Orientation), or `static Microsoft.UI.Reactor.Factories`. (A close-but-wrong factory name instead raises `REACTOR_DYM_003` above.) |
| `CS1061` | error | "'X' does not contain a definition for 'Y'" | A type-specific modifier called on an element type that does not declare it — `.Bold()` / `.FontSize()` / `.TextWrapping()` are `TextBlockElement`-only, so `Button("Go").Bold()` fails. This is **not** an ordering problem: modifiers are generic (`<T> where T : Element`) and preserve the concrete type, so `.Margin(16).Bold()` and `.Bold().Margin(16)` compile alike. Check the receiver's element type against `reactor-dsl/references/reactor.api.txt`. |
| `CS0117` | error | "'Element' does not contain a definition for X" | You're calling a factory or static member that doesn't exist — confirm the name against `reactor-dsl/references/reactor.api.txt`. (For a missing *instance* modifier see `CS1061` above.) |
Expand Down
4 changes: 2 additions & 2 deletions samples/ReactorHostControlDemo/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ public MainWindow()
).Margin(16, 8, 16, 16),

items.Count == 0
? TextBlock("All done!").Foreground("#888").HAlign(HorizontalAlignment.Center).Margin(16)
? TextBlock("All done!").Foreground("#888888").HAlign(HorizontalAlignment.Center).Margin(16)
: TextBlock($"{items.Count} item{(items.Count == 1 ? "" : "s")} remaining")
.Foreground("#888").HAlign(HorizontalAlignment.Center).Margin(0, 0, 0, 16)
.Foreground("#888888").HAlign(HorizontalAlignment.Center).Margin(0, 0, 0, 16)
);
});
TodoPanel.Child = todoHost;
Expand Down
Loading
Loading