You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
- 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`.
55
56
-**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**.
(Names to bikeshed; semantics fixed: *input vs derived vs independent*, plus *ctor-eager* flags.)
66
67
67
68
**c) Hidden hooks** (`EditorBrowsable(Never)`, emitted by default — opt-out via `EnableFeedMocking(IsEnabled = false)`):
69
+
68
70
- 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}`;
69
71
- 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).
2.`IMessageEntry` → the view lazily creates **one entry-driven wrapper feed** (`MessageEntryFeed`, internal) and keeps it for the lifetime of the subscription.
132
135
3. anything else → today's behavior (ignored). No heuristic.
Copy file name to clipboardExpand all lines: specs/013-mvux-mocking-previews/history.md
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,7 @@
1
1
# 013 — Historique des versions et décisions
2
2
3
3
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
+
4
5
- fichiers **recovery** locaux (reconstruits depuis les transcripts) — portaient la question ouverte « context scope » et la référence au commit `cd4c9ad` ;
5
6
- 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`) ;
6
7
- transcript Telegram complet de la discussion.
@@ -12,6 +13,7 @@ Reconstruction après la perte du workspace ACO (`devid-feat-uno-extensions-arch
12
13
**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 ».
13
14
14
15
**Discussion & décisions :**
16
+
15
17
- 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).
16
18
- 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é).
17
19
- 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
**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
+
24
27
-`__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).
25
28
-**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`**.
26
29
-**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
47
50
## v4 — révisions de David dans VS Code (commit `cd4c9ad`, perdu ; contenu = son `archi.md` joint)
48
51
49
52
Réponses de David à ma question « OK avec ce découpage ? » — par édition directe de l'architecture :
53
+
50
54
-**`MessageEntry` reste un plain CLR object dans Core** — délibérément **PAS** un `DependencyObject` (aucune complexité property-system UI dans le message model).
51
55
-**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.
52
56
- 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
71
75
- 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.
72
76
- Répercuté dans les 3 volets : spec §13 + G9 + R7 + D10, archi §1/§6/§7, impl §1/§2.2/§6/§7/§8/§9.
73
77
74
-
75
78
## v7 — décision de David (dim. 24/08, soir) — gate per-context + swap réflexif
76
79
77
80
-**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
## v9 — réconciliation avec #3149 (FeedMock mergé) + review David (mar. 25/08)
105
108
106
109
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
113
116
114
117
**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é).
115
118
116
-
117
119
## v10 — review David (commentaire 31) : AsyncLocal hors de Core
118
120
119
121
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
### 2.1 Dependency attributes (emitted by MVUX gen AND hand-declarable; explicit wins/merges)
22
+
22
23
```csharp
23
24
namespaceUno.Extensions.Reactive.Config;
24
25
@@ -39,14 +40,17 @@ public sealed class CtorDependencyAttribute : Attribute
39
40
publicboolEager { get; init; } // true → NRE under null-inject; Create must require it
40
41
}
41
42
```
43
+
42
44
(David's `[FeedShape("Steps", ModelParameter=…)]` idea, renamed. Multiple per member allowed.)
43
45
44
46
### 2.2 Mockable gate + swap anchor
47
+
45
48
-**`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`.
46
49
- 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).
47
50
-**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).
48
51
49
52
### 2.3 Tier-1 core surfaces
53
+
50
54
-`Feed.Value<T>` public factory (from #3148, additive).
51
55
- 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.
52
56
- 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
66
70
## 4. Mocking package (`Uno.HotTesting.Reactive`)
67
71
68
72
### 4.1 Runtime vocabulary (all generic and strongly typed)
73
+
69
74
```csharp
70
75
publicstaticclassFeedMock
71
76
{
@@ -92,10 +97,13 @@ public static class CommandMock
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).
96
102
97
103
### 4.2 Generator (runs in the consumer/test project, metadata-driven)
104
+
98
105
For each Model/VM pair found in referenced assemblies with `__Mock_*` hooks + attributes:
106
+
99
107
```csharp
100
108
publicrecordRecipeModelMock
101
109
{
@@ -112,7 +120,9 @@ public static partial class RecipeViewModelMock // partial → user extends wi
- Required properties = the **ServiceDependent** input set; `Create` takes only the record (no denormalized per-input overloads).
117
127
-**Derived members: optional overrides** — `null` (default) → real derivation recomputes over swapped inputs; set → that member's wrapper is swapped too. Independent members: untouched.
118
128
-**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:
121
131
- Diagnostic `MOCK0001` when a VM is reachable but its assembly lacks hooks (opt-in missing).
122
132
123
133
## 5. UI (`Uno.Extensions.Reactive.UI`) — tier 1
134
+
124
135
-`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.
125
136
-**Mutations of an already-assigned entry are not observed** (plain CLR, not observable); a new instance is the unit of change.
126
137
- XAML element syntax (`<reactive:MessageEntry IsProgress="True" />`, …) — examples in architecture §3.
@@ -149,6 +160,7 @@ var vm = RecipeViewModelMock.Create(new RecipeModelMock { Steps = ListFeedMock.V
149
160
**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.
150
161
151
162
Mechanism (resolved against source — `Core/Internal/SourceContext.cs`, D12):
163
+
152
164
-**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`.
153
165
-**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`.
154
166
-**Ambient propagation**: the existing `AsyncLocal<SourceContext> Current` carries mockability across async construction; no bespoke `AsyncLocal`.
- Byte-identical output when opted out; hooks hidden (`EditorBrowsable`) and typed (concrete generics).
183
197
- 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**.
184
198
185
199
### Runtime / UI (Skia)
200
+
186
201
- Each pinned state renders; Loading keeps `IsExecuting`.
187
202
- Successive `Source` entries evolve without re-subscribe (no loading flash); **mutating an assigned entry does not emit** — assigning a replacement does.
188
203
-`SetMock` drives Loading → Value → Error live; derived member updates on-screen after an input swap (D6 end-to-end).
189
204
- Command states drive `Button.IsEnabled`; hot reload does not clobber a mocked VM/context.
190
205
191
206
### Scoped activation (with §6 spike)
207
+
192
208
-**Context not mockable → feeds are the raw instances** (no `HotSwapFeed` in the cache, no measurable overhead) — the G9 guard test.
193
209
-**Fail-hard swap**: a mocked member with no `IHotSwapState<T>` throws (D11), asserted.
194
210
- Assembly-init scope covers every test of the run; a per-test scope covers only its own.
195
211
- 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`.
196
212
197
213
### Contract freeze
214
+
198
215
- Reflection-discovery test for `{Model}Mock`/`Empty`/`Create`/`SetMock`/attribute names (Hot Design contract).
199
216
200
217
## 9. Docs
218
+
201
219
-`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.
202
220
-`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).
Copy file name to clipboardExpand all lines: specs/013-mvux-mocking-previews/spec.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -204,6 +204,7 @@ No new mechanism: each overload is `Create()` + a `SetMock` of §7, so a preview
204
204
## 9. Goals / Non-goals
205
205
206
206
**Goals**
207
+
207
208
- G1. Pin any service-dependent feed / list-feed / state / command of a real generated VM.
208
209
- G2. **Derived feeds recompute over mocked inputs** (business logic survives); derived members remain individually overridable for tests.
209
210
- 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
215
216
- 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.
216
217
217
218
**Non-goals**
219
+
218
220
- NG1. Behavioral/integration testing of services (this targets presentation state).
219
221
- 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.
220
222
- NG3. Making arbitrary JSON graphs bindable on every platform (WinAppSDK dynamic-binding caveat).
0 commit comments