Skip to content

Commit 149b987

Browse files
committed
fix(ci): add the missing XML docs and fix markdownlint on the specs
The repository enables GenerateDocumentationFile together with TreatWarningsAsErrors, so a public member without an XML comment fails the Release build even though a local Debug build stays green. Added /// <inheritdoc /> on FeedsMockGenerator.Initialize and Execute, the same way FeedsGenerator already does. Also fixed MD032, MD012, MD022 and MD031 on the four spec 013 documents.
1 parent 29714cd commit 149b987

5 files changed

Lines changed: 30 additions & 5 deletions

File tree

specs/013-mvux-mocking-previews/architecture.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ Identity risk (R6): lambdas capturing locals/params produce fresh delegate targe
5151
### 2.1 MVUX generator (Model's assembly — analysis + attributes + hidden hooks)
5252

5353
**a) Dependency analysis** (Roslyn, source available):
54+
5455
- per feed/command member: walk initializer/getter body; **lambda/anonymous/local-function bodies = deferred boundary**; eager remainder binding to a ctor param (or param-assigned field) → `ServiceDependent(param)`; reference to another feed member → `DerivedFrom(member)`; else `Independent`.
5556
- **ctor instrumentation**: walk ctor bodies (incl. field/property initializers, primary-ctor captures used eagerly); any eager service dereference → the ctor is **unsafe under null-inject for that parameter**.
5657

@@ -65,6 +66,7 @@ Identity risk (R6): lambdas capturing locals/params produce fresh delegate targe
6566
(Names to bikeshed; semantics fixed: *input vs derived vs independent*, plus *ctor-eager* flags.)
6667

6768
**c) Hidden hooks** (`EditorBrowsable(Never)`, emitted by default — opt-out via `EnableFeedMocking(IsEnabled = false)`):
69+
6870
- on the **Model partial**: **nothing per-feed** — the swap is reflection over `IHotSwapState<T>` members at runtime (D11), reusing the hot-reload driver, fail-hard. The generator emits no `__Mock_Swap_{Member}`;
6971
- on the **VM partial**: **no construction seam** — null-inject uses the existing public ctors (`new {Vm}(default!, …)`) under an ambient `MockingService.Enable()` scope (D12: the `SourceContext` built at construction is mockable, captured on the instance). The only emitted seam is `__Mock_SetCommand(string name, IAsyncCommand)` (public, `EditorBrowsable(Never)`, fail-hard) which reassigns a command property post-construction — commands have no `IHotSwapState<T>` and are unreachable by the reflection swap (R2).
7072

@@ -127,6 +129,7 @@ public sealed class AxisValue
127129
### Coercion & evolution semantics
128130

129131
`FeedView.OnSourceChanged`:
132+
130133
1. `ISignal<IMessage>` (any feed/state) → passthrough, unchanged.
131134
2. `IMessageEntry` → the view lazily creates **one entry-driven wrapper feed** (`MessageEntryFeed`, internal) and keeps it for the lifetime of the subscription.
132135
3. anything else → today's behavior (ignored). No heuristic.

specs/013-mvux-mocking-previews/history.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# 013 — Historique des versions et décisions
22

33
Reconstruction après la perte du workspace ACO (`devid-feat-uno-extensions-architecture`, détruit avec la branche `dev/devid/spec-013-mvux-mocking` non poussée). Sources du merge :
4+
45
- fichiers **recovery** locaux (reconstruits depuis les transcripts) — portaient la question ouverte « context scope » et la référence au commit `cd4c9ad` ;
56
- fichiers **VS Code de David** (joints le 23/08 18:22) — `spec.md`/`impl.md` = état v1 (`8d589d9`, non rechargés), `archi.md` = état le plus récent (v4, post-`2618def`) ;
67
- transcript Telegram complet de la discussion.
@@ -12,6 +13,7 @@ Reconstruction après la perte du workspace ACO (`devid-feat-uno-extensions-arch
1213
**Contexte.** Objectif posé par David : helpers de mocking pour les previews UI (Hot Design) et le testing d'apps consommant des feeds (simuler les états des feeds, pas tester les feeds). Deux POCs existants : PR **#3148** (Nick, spec 009 — XAML only, enveloppe POCO/JSON coercée dans `FeedView.Source`) et PR **#3147** (Steve, spec 012 — vocabulaire `Mocks` + générateur `{Vm}Mocks`/`CreateMock`). Vision à 3 niveaux de David : (1) statique dans le XAML, (2) structures de mock par-feed d'un VM, (3) helpers « modèle complet ».
1314

1415
**Discussion & décisions :**
16+
1517
- Mon premier retour (socle 3147 + markup extension + catalogue) recadré par David : partir de **SON design**`MessageEntry` pour la couche 1 (pas d'enveloppe magique, JSON→dynamic) et le **SwapFeed du hot-reload** pour la couche 2 (contrôle total, système 100 % malléable ; la couche 3 ne devient que des helpers au-dessus).
1618
- Faisabilité vérifiée dans le code : `MessageEntry<T>`/`IMessageEntry` publics ; `MessageEntry.Empty` force l'axe Data → tue le canari « Undefined » (spec 012 §10.2) ; `HotSwapFeed`/`IHotSwapState`/`StateImpl` = seam existant (seul appelant : hot-reload) ; gate `HotReloadSupport.State` ; commandes non swap-backed (gap identifié).
1719
- Construction du VM : ni « vrai VM via DI » ni « ctor sans modèle » → **vrai VM + vrai Model**, services **null-injectés**, prouvé sûr par **analyse de dépendances au codegen** (« option 2 » de David). Fondement : les feeds MVUX sont des arrow-getters lazy (service capturé en closure, touché à l'énumération seulement) ; cas bloquant = accès service **eager dans le ctor**. « On ne contrôle pas comment nos users utilisent notre archi » → l'analyse + diagnostics sont obligatoires.
@@ -21,6 +23,7 @@ Reconstruction après la perte du workspace ACO (`devid-feat-uno-extensions-arch
2123
## v1 — commit `8d589d9` (dim. 23/08 10:42) — le pivot « génération extérieure » + checkpoint
2224

2325
**Discussion (23/08 matin) :** David réalise en review que le mocking doit être **consommable de l'extérieur** (projet de test qui référence l'app) → on ne peut pas injecter le code dans le VM/Model ; le gen MVUX ajoute des **hooks cachés** (sur le modèle de HR) et le gen de mocking prend le contrôle depuis l'extérieur. Son dump : `RecipeModelMock` record `required init` + `Empty`, `Create()`/`Create(steps)` (null-inject + `SetModel`), `SetModel``__Reactive_UpdateModel`. Mes vérifications ont ajouté :
26+
2427
- `__Reactive_UpdateModel` inutilisable tel quel (réassigne `__reactiveModel`, `Unsafe.As` sur type étranger = UB) → **méthode dédiée cachée** (confirmé par David, pt 3).
2528
- **Dérivés doivent survivre** (pt « c'est tout le concept ») → découverte de l'ancrage : les feeds sont cachés par `AttachedProperty.GetOrCreate` avec identité stable → **wrap `HotSwapFeed` au niveau du cache Model-feed** ; les dérivations composent sur le wrapper → le swap traverse la logique métier ; `SetModel` = swaps typés, **plus de `dynamic`**.
2629
- **Attributs de dépendances** émis par l'analyse ET déclarables à la main (idée `[FeedShape(...)]` de David, renommée `[FeedDependency]`/`[CtorDependency]`) — nécessaires car le gen externe n'a pas les syntax trees.
@@ -47,6 +50,7 @@ Reconstruction après la perte du workspace ACO (`devid-feat-uno-extensions-arch
4750
## v4 — révisions de David dans VS Code (commit `cd4c9ad`, perdu ; contenu = son `archi.md` joint)
4851

4952
Réponses de David à ma question « OK avec ce découpage ? » — par édition directe de l'architecture :
53+
5054
- **`MessageEntry` reste un plain CLR object dans Core** — délibérément **PAS** un `DependencyObject` (aucune complexité property-system UI dans le message model).
5155
- **L'entry n'est pas observable** : muter `Data`/`Error`/`IsProgress`/`Axes` après assignation ne pousse rien ; **remplacer l'instance** est l'unité de changement.
5256
- Le converter JSON **n'est plus un livrable** : illustration **app-owned** attachée à `FeedView.Source`, doit retourner `IMessageEntry` ; la spec ne définit ni n'implémente de converter.
@@ -71,7 +75,6 @@ Puis : **perte du workspace ACO** (node détruit, branche non poussée — commi
7175
- Reste au spike (P0-e) le **mécanisme seul** (contexte propriétaire, eager/lazy, `AsyncLocal` vs token porté, imbrication, concurrence, survie après `Dispose`, câblage vers le flag D4) — plus la forme de l'API.
7276
- Répercuté dans les 3 volets : spec §13 + G9 + R7 + D10, archi §1/§6/§7, impl §1/§2.2/§6/§7/§8/§9.
7377

74-
7578
## v7 — décision de David (dim. 24/08, soir) — gate per-context + swap réflexif
7679

7780
- **Question tranchée (« où vit le flag mockable ? »)** : investigation source demandée par David.
@@ -95,12 +98,12 @@ Landée sur `dev/devid/spec-013-mvux-mocking` (poussée staging PR #1), après l
9598
**Tests (réellement exécutés) :** Given_MockingActivation 4/4, Given_MockingRuntime 4/4, Given_GeneratedMock 4/4 (Create+SetModel → VM réel → feed mické ; live re-swap ; `Create()` Empty→None ; override commande), Tests.Generator 80/80 (byte-identique préservé), Given_HotReload 8/8 (Core inchangé).
9699

97100
**Reste (hors périmètre du cœur tier 2/3, à planifier avec David) :**
101+
98102
- `MockFeed.Message`/`Script` (dépendent du vocabulaire de #3147, non mergé).
99103
- Diagnostics `FEED3201–3203` / `MOCK0001` (analyse en place, diagnostics non émis).
100104
- Docs `doc/Learn/Mvux/Testing.md` + `FeedView.md` (§9), Tier 1 (on hold).
101105
- Remontée github : outbox ABO → PR #3165 (après review David).
102106

103-
104107
## v9 — réconciliation avec #3149 (FeedMock mergé) + review David (mar. 25/08)
105108

106109
Rebase sur `main` (PR #3154 / issue #3149 mergée) : **le vocabulaire de feeds mockés existe déjà** dans une assembly dédiée `Uno.HotTesting.Reactive` (`FeedMock`/`ListFeedMock`, namespace + assembly `Uno.HotTesting.Reactive`, spec 009). Mon `Uno.Extensions.Reactive.Mocking` le dupliquait → **supprimé**. Décisions de naming/namespace suite à la review de David sur la staging PR #1 :
@@ -113,7 +116,6 @@ Rebase sur `main` (PR #3154 / issue #3149 mergée) : **le vocabulaire de feeds m
113116

114117
**Tests après refactor (verts) :** Given_MockingActivation 4/4, Given_MockingRuntime 4/4, Given_GeneratedMock 4/4, Tests.Generator 80/80, `Uno.HotTesting.Reactive.Tests` 22/22 (FeedMock existant non régressé).
115118

116-
117119
## v10 — review David (commentaire 31) : AsyncLocal hors de Core
118120

119121
Retour de David sur `SourceContext` : *« si on a besoin d'un AsyncLocal pour le mocking, ça n'apporte rien de le mettre dans le SourceContext, on devrait le garder dans le MockingService »*. Juste — l'état d'activation ambient est une préoccupation **mocking**, pas Core.
@@ -124,7 +126,6 @@ Retour de David sur `SourceContext` : *« si on a besoin d'un AsyncLocal pour le
124126

125127
Tests inchangés/verts : Given_MockingActivation 4/4, Given_MockingRuntime 4/4, Given_GeneratedMock 4/4, Tests.Generator 80/80, Uno.HotTesting.Reactive.Tests 22/22.
126128

127-
128129
## v11 — doc + sample + polish naming factory (mar. 25/08)
129130

130131
- **Doc** : `doc/Reference/Reactive/testing.md` (celle de #3149 sur `FeedMock` hand-written) étendue avec la couche générée tier 2/3 : scope `MockingService.Enable()`, `record {Model}Mock` (inputs required, derived + commandes optionnels), `{Vm}Mock.Create(...)`, `vm.SetModel(...)`, `CommandMock`, derived-survives, one-liners + catalogs nommés (tier 3), opt-out `[assembly: EnableFeedMocking(IsEnabled = false)]`. La phrase « no generator » de #3149 est mise à jour.
@@ -133,7 +134,6 @@ Tests inchangés/verts : Given_MockingActivation 4/4, Given_MockingRuntime 4/4,
133134

134135
Tests : MockingActivation 4/4, MockingRuntime 4/4, GeneratedMock 5/5, Uno.HotTesting.Reactive.Tests 22/22, Tests.Generator 80/80.
135136

136-
137137
## v12 — review David (post-discussion staging PR #1, ven. 28/08)
138138

139139
Six retours de David sur la PR, tous appliqués :

specs/013-mvux-mocking-previews/implementation.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Concrete surfaces, touch-list, phasing, tests. Names bikesheddable; semantics fi
1919
## 2. Core (`Uno.Extensions.Reactive`)
2020

2121
### 2.1 Dependency attributes (emitted by MVUX gen AND hand-declarable; explicit wins/merges)
22+
2223
```csharp
2324
namespace Uno.Extensions.Reactive.Config;
2425

@@ -39,14 +40,17 @@ public sealed class CtorDependencyAttribute : Attribute
3940
public bool Eager { get; init; } // true → NRE under null-inject; Create must require it
4041
}
4142
```
43+
4244
(David's `[FeedShape("Steps", ModelParameter=…)]` idea, renamed. Multiple per member allowed.)
4345

4446
### 2.2 Mockable gate + swap anchor
47+
4548
- **`SourceContext.IsMockingActive`** (per-context bit, D12 — distinct from `HotReload`, no global static, no bespoke `AsyncLocal`) — **set by the activation scope (§6), off by default**; context not mockable → no wrap, so a live app pays nothing (spec G9/R7). Read at wrap time in `StateImpl` ctor **instead of** `FeedConfiguration.EffectiveHotReload`.
4649
- When the owning context is mockable: feed factories wrap the cached instance in `HotSwapFeed<T>` (the wrapper IS the cached value → stable identity; derivations compose on the wrapper). Minimal wiring: wrap inside `AttachedProperty.GetOrCreate` call sites in `Core/Feed.cs` / `Core/ListFeed.cs` factories (one helper reading the context bit).
4750
- **Swap = reflection over the context's `IHotSwapState<T>` members** (D11), reusing the hot-reload driver (`BindableViewModelBase.HotReload`), **fail-hard**: a mocked member that cannot be swapped throws (no silent skip — the hot-reload delta).
4851

4952
### 2.3 Tier-1 core surfaces
53+
5054
- `Feed.Value<T>` public factory (from #3148, additive).
5155
- Authorable non-generic `MessageEntry : IMessageEntry`**plain CLR object, not a `DependencyObject`, not observable**; settable `Data` / `IsUndefined` / `Error` / `IsProgress`; `Axes` (`AxisValueCollection` of `AxisValue { string Axis; object? Value }`) + `Set(MessageAxis, object?)` code path.
5256
- Axis-identifier resolution against core + registered app axes; **unknown identifier → diagnostic**, never a silent drop.
@@ -66,6 +70,7 @@ On by default (the runtime decides activation). Opt-out: `[assembly: EnableFeedM
6670
## 4. Mocking package (`Uno.HotTesting.Reactive`)
6771

6872
### 4.1 Runtime vocabulary (all generic and strongly typed)
73+
6974
```csharp
7075
public static class FeedMock
7176
{
@@ -92,10 +97,13 @@ public static class CommandMock
9297
}
9398
public enum FeedMockState { Undefined, Loading, Empty, Value, Error, Refreshing }
9499
```
100+
95101
Built over public `Feed.Create` + `MessageBuilder` (vocabulary from #3147). **These APIs never accept the non-generic tier-1 `MessageEntry` or untyped envelopes.** Never referenced by a published app head (non-AOT, dev/test only — NG2/D7).
96102

97103
### 4.2 Generator (runs in the consumer/test project, metadata-driven)
104+
98105
For each Model/VM pair found in referenced assemblies with `__Mock_*` hooks + attributes:
106+
99107
```csharp
100108
public record RecipeModelMock
101109
{
@@ -112,7 +120,9 @@ public static partial class RecipeViewModelMock // partial → user extends wi
112120
public static void SetMock(this RecipeViewModel vm, RecipeModelMock mock); // typed swaps (fail-hard)
113121
}
114122
```
123+
115124
Rules:
125+
116126
- Required properties = the **ServiceDependent** input set; `Create` takes only the record (no denormalized per-input overloads).
117127
- **Derived members: optional overrides**`null` (default) → real derivation recomputes over swapped inputs; set → that member's wrapper is swapped too. Independent members: untouched.
118128
- **Command mocking is deferred to vNext**: the record carries no command member and `SetMock` wires none (the MVUX `__Mock_SetCommand` seam stays available for that future work).
@@ -121,6 +131,7 @@ Rules:
121131
- Diagnostic `MOCK0001` when a VM is reachable but its assembly lacks hooks (opt-in missing).
122132

123133
## 5. UI (`Uno.Extensions.Reactive.UI`) — tier 1
134+
124135
- `FeedView.OnSourceChanged`: typed branch `IMessageEntry` → lazily create ONE `MessageEntryFeed` wrapper kept across `Source` changes; a subsequent `IMessageEntry` instance is **pushed** into the wrapper (subscription preserved, no state reset — natural-evolution contract, architecture §3). No heuristic.
125136
- **Mutations of an already-assigned entry are not observed** (plain CLR, not observable); a new instance is the unit of change.
126137
- XAML element syntax (`<reactive:MessageEntry IsProgress="True" />`, …) — examples in architecture §3.
@@ -149,6 +160,7 @@ var vm = RecipeViewModelMock.Create(new RecipeModelMock { Steps = ListFeedMock.V
149160
**Non-negotiable constraint:** context not mockable → **no `HotSwapFeed` wrap at all**. The wrap is one indirection per feed; it may never be injected into the feeds of a live app (spec G9/R7). `SourceContext.IsMockingActive` (§2.2, D12) is the internal per-context gate the scope drives, not a switch app authors set.
150161

151162
Mechanism (resolved against source — `Core/Internal/SourceContext.cs`, D12):
163+
152164
- **Owner context = `SourceContext`** — already owns `States`/subscriptions, already ambient via `AsyncLocal<SourceContext> Current`, already per-owner via `GetOrCreate(owner)`, with an eager pre-seed seam `PreConfigure(type, ctx)` / `Set(owner, ctx)`. It gains `bool IsMockingActive`.
153165
- **Eager vs lazy = solved by pre-seed**: `Create(...)` pre-seeds a mockable context on the VM/Model owner (`PreConfigure`/`Set`), so a lazy first subscription after the `using` block still wraps — the bit is on the context instance, not only on the ambient `AsyncLocal`.
154166
- **Ambient propagation**: the existing `AsyncLocal<SourceContext> Current` carries mockability across async construction; no bespoke `AsyncLocal`.
@@ -171,33 +183,39 @@ Mechanism (resolved against source — `Core/Internal/SourceContext.cs`, D12):
171183
## 8. Test plan
172184

173185
### Core
186+
174187
- Every typed `FeedMock`/`ListFeedMock`/`CommandMock` state emits expected axes.
175188
- Authorable entry maps to Data/Error/Progress/Undefined correctly; custom axes map and diff correctly.
176189
- Consecutive entry instances produce correct core + custom axis diffs.
177190
- Wrap identity (`AttachedProperty` returns the same wrapper); swap propagation through `Select`/`Where` and chained derived feeds; live re-swap.
178191

179192
### Generators
193+
180194
- Classification fixtures (lazy/eager/derived/independent; ctor bodies, field/property initializers, primary-ctor captures).
181195
- Attribute emission; explicit-attribute override/merge; FEED3201–3203.
182196
- Byte-identical output when opted out; hooks hidden (`EditorBrowsable`) and typed (concrete generics).
183197
- Consumer generation against a compiled fixture assembly; required-input set = ServiceDependent set; eager-ctor → required service parameter; MOCK0001; **no tier-1/untyped surface in tier-2/3 output**.
184198

185199
### Runtime / UI (Skia)
200+
186201
- Each pinned state renders; Loading keeps `IsExecuting`.
187202
- Successive `Source` entries evolve without re-subscribe (no loading flash); **mutating an assigned entry does not emit** — assigning a replacement does.
188203
- `SetMock` drives Loading → Value → Error live; derived member updates on-screen after an input swap (D6 end-to-end).
189204
- Command states drive `Button.IsEnabled`; hot reload does not clobber a mocked VM/context.
190205

191206
### Scoped activation (with §6 spike)
207+
192208
- **Context not mockable → feeds are the raw instances** (no `HotSwapFeed` in the cache, no measurable overhead) — the G9 guard test.
193209
- **Fail-hard swap**: a mocked member with no `IHotSwapState<T>` throws (D11), asserted.
194210
- Assembly-init scope covers every test of the run; a per-test scope covers only its own.
195211
- Nested `Enable()` scopes restore correctly; parallel tests do not leak mockability; async construction retains the intended scope; lazy first subscription after scope disposal has defined behavior; existing contexts remain deterministic after `Dispose`.
196212

197213
### Contract freeze
214+
198215
- Reflection-discovery test for `{Model}Mock`/`Empty`/`Create`/`SetMock`/attribute names (Hot Design contract).
199216

200217
## 9. Docs
218+
201219
- `doc/Learn/Mvux/Testing.md`: `MockingService.Enable()` scope (assembly-init vs per-test, and why it is never app-wide), typed vocabulary, `Create`/`SetMock`, derived-feeds-survive concept, eager-ctor guidance, non-AOT constraint, R4/R5 caveats.
202220
- `doc/Learn/Mvux/FeedView.md`: tier-1 entry authoring + custom axes; converter shown only as an application-owned illustration at `FeedView.Source` (not a deliverable).
203221
- `rules.md`: FEED3201–3203, MOCK0001.

specs/013-mvux-mocking-previews/spec.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ No new mechanism: each overload is `Create()` + a `SetMock` of §7, so a preview
204204
## 9. Goals / Non-goals
205205

206206
**Goals**
207+
207208
- G1. Pin any service-dependent feed / list-feed / state / command of a real generated VM.
208209
- G2. **Derived feeds recompute over mocked inputs** (business logic survives); derived members remain individually overridable for tests.
209210
- G3. Mock generation happens **in the consumer project** (test/preview), against app metadata.
@@ -215,6 +216,7 @@ No new mechanism: each overload is `Create()` + a `SetMock` of §7, so a preview
215216
- G9. **Zero cost on a live app**: the `HotSwapFeed` wrap is created only for feeds built inside an explicit activation scope (§13). No wrapper is ever injected into the feeds of a running application.
216217

217218
**Non-goals**
219+
218220
- NG1. Behavioral/integration testing of services (this targets presentation state).
219221
- NG2. **AOT/trim compliance of the mocking path.** Mocking is dynamic injection, dev/test-time only (JIT). Accepted and documented; never ships in a published app.
220222
- NG3. Making arbitrary JSON graphs bindable on every platform (WinAppSDK dynamic-binding caveat).

0 commit comments

Comments
 (0)