diff --git a/ROADMAP.md b/ROADMAP.md index ee95660e..62f74247 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -96,7 +96,9 @@ Anthropic's constrained decoding enforces property names, types, and `additional - Kubernetes — deferred to public beta per DEC-032. - Garmin Connect integration — deferred to post-MVP-1; Apple Health prioritized per DEC-033. - Frontend visual design planning — flagged, not yet started. -- **Marten 9 upgrade** — current pin is Marten 8.28; Marten 9 (undated) drops sync LINQ ops (tied to Npgsql 10), flips Conjoined PK ordering to `TenantId_Then_Id`, and will formally certify .NET 10. Both changes are mechanical — sync removal is a pass with `LoadAsync`-style replacements, PK reorder is a one-time index-rebuild migration. No load-bearing rewrite risk. Monitor `JasperFx/marten` repo; revisit when v9 ships. If a `.net10`-specific Marten 8 bug surfaces before v9 lands, the escape hatch is targeting the test assembly at `net9.0` while keeping the SUT on `net10.0`. Captured per R-047. +- **Marten 9 / Wolverine 6 upgrade** — ✅ Shipped 2026-05-30 (DEC-071, PR #125): Marten 9.2.1 + Wolverine 6.1.0 on JasperFx 2.0. Remaining levers deferred: + - **QuickAppend append mode** — Marten 9 made `QuickWithServerTimestamps` the default; we deliberately kept `EventAppendMode.Rich` (DEC-071). ~50% append throughput + fewer skips under contention. Adopt only after validating nothing depends on Rich's client-side timestamps/metadata. Not a POC constraint; revisit at pre-MVP-0 scale. + - **`Marten.PgVector` for the coaching/LLM layer** — Marten 9.3 ships `UsePgVector()` + a `VectorProjection` base; embeddings live in the same Postgres. Candidate for similar-athlete / session-history retrieval feeding LLM context. Evaluate if RAG-style retrieval enters scope. ### Cost optimization (post-MVP-0, DEC-038) diff --git a/docs/decisions/decision-log.md b/docs/decisions/decision-log.md index b781d849..c0cece88 100644 --- a/docs/decisions/decision-log.md +++ b/docs/decisions/decision-log.md @@ -2652,4 +2652,32 @@ T01.1 implementation surfaced two divergences from this DEC's estimates. (1) The --- +## DEC-071: Critter Stack 2026 upgrade — Marten 9 / Wolverine 6; kept `Rich` append mode; `RuntimeCompilation` required for dev/test codegen + +**Date:** 2026-05-30 +**Category:** Backend / Persistence / Event sourcing / Build +**Status:** Accepted +**Drives:** Keeps the event-sourcing + outbox backbone current on .NET 10 and puts the production path on the AOT-ready, Roslyn-free `TypeLoadMode.Static` track. +**Cites:** R-076 (`docs/research/artifacts/batch-27a-critter-stack-9-6-migration.md`); PR #125. +**Builds on:** DEC-048, DEC-049 (Marten + Wolverine startup composition), DEC-067 (event upcasting — verified intact). + +**Decision:** Upgraded the full Critter Stack together — Marten / Marten.EntityFrameworkCore 8.37.1 → 9.2.1, WolverineFx / .EntityFrameworkCore / .Marten 5.39.3 → 6.1.0, on JasperFx 2.0 — consolidating the four dependabot PRs (#119–#122) that each failed CI in isolation because the five packages are interdependent. Accepted the new 9/6 performance defaults (source-generated projections, lightweight sessions, System.Text.Json, `EnableAdvancedAsyncTracking` on) **except** deliberately kept `EventAppendMode.Rich` rather than adopt the new `QuickWithServerTimestamps` (QuickAppend) default. Did **not** use `RestoreV8Defaults()`. Added `WolverineFx.RuntimeCompilation` 6.1.0 on the dev/test path only. + +**Rationale:** + +- **`Rich` kept on purpose.** Marten 9 flipped the append-mode default to QuickAppend. Our append metadata semantics assume Rich, so the explicit `Rich` is now load-bearing rather than coincidental. QuickAppend's ~50% throughput gain is a deferred lever (ROADMAP) pending a validation pass that nothing depends on Rich's client-side timestamps/metadata — throughput is not a POC constraint yet. +- **`WolverineFx.RuntimeCompilation` is now required wherever the host boots `TypeLoadMode.Auto`** (dev runs + the integration-test host) because Wolverine 6 extracted the runtime Roslyn compiler out of core. Without it the host throws `No IAssemblyGenerator is registered`. Production stays `TypeLoadMode.Static` (pre-generated) and ships with no Roslyn — the intended design and a cold-start win. This extends DEC-048's composition; the self-contained rationale lives in `RunCoach.Api.csproj` at the reference. +- **Convention-method `SingleStreamProjection` subclasses must be `partial`** — Marten 9 dispatches `Apply`/`Create`/`ShouldDelete` via the compile-time `JasperFx.Events` source generator (shipped in the Marten analyzer asset); the runtime reflection fallback was removed. `OnboardingProjection` and `PlanProjection` became `partial`; `UserProfileFromOnboardingProjection` uses an explicit `ApplyEvent` override and is unaffected. Self-documented in the projection classes. +- **Composition otherwise unchanged.** `IntegrateWithWolverine()`-alone envelope wiring (DEC-048) is still the sole path and correct in 6.x. The `ServiceLocationPolicy` → `NotAllowed` default flip does not affect our constructor-injection DI. The reflection-based per-event schema-version helper (`MapEventTypeWithSchemaVersion` / `EventStoreOptionsExtensions`) is byte-identical in 9.2.1 and left as-is. + +**Verified:** `dotnet build` clean (0 warnings under `TreatWarningsAsErrors`), full suite **1124/1124** on Testcontainers Postgres (Solo async daemon + `ApplyAllDatabaseChangesOnStartup` clean boot; upcaster and idempotency error-routing tests green); CI green on PR #125 including the OpenAPI drift gate (zero API-contract drift). + +**Alternatives considered:** + +- **`RestoreV8Defaults()` for a zero-behavior-change upgrade** — rejected. We want the new performance defaults; the only one with semantic risk (Rich → QuickAppend) was overridden explicitly, so the blanket restore would needlessly forfeit the rest. +- **Merging the four dependabot PRs (#119–#122) individually** — impossible; `WolverineFx.Marten 6.x` requires `WolverineFx 6.x` and `Marten 9.x`, so each PR failed alone. Consolidated into one branch. +- **Pre-generating Wolverine code (`dotnet run -- codegen write`) + `Static` everywhere** to avoid the new dependency — rejected for dev/test; it reintroduces the on-disk generated-file hazard the in-memory codegen avoids. `RuntimeCompilation` is the lower-risk path for the test suite. + +--- + *Add new decisions at the bottom. Use format: DEC-XXX, date, category, decision, rationale, alternatives.* diff --git a/docs/research/artifacts/batch-27a-critter-stack-9-6-migration.md b/docs/research/artifacts/batch-27a-critter-stack-9-6-migration.md new file mode 100644 index 00000000..be285dc2 --- /dev/null +++ b/docs/research/artifacts/batch-27a-critter-stack-9-6-migration.md @@ -0,0 +1,124 @@ +# R-076 / Batch 27a — Critter Stack 2026 upgrade (Marten 9 / Wolverine 6) migration map + +**Status:** Integrated (DEC-071) · **Date:** 2026-05-30 · **Drove:** PR #125 + +Consolidated, primary-source-verified migration notes for upgrading RunCoach's +event-sourcing + outbox backbone from Marten 8.37.1 / Wolverine 5.39.3 to the +"Critter Stack 2026" wave (Marten 9.2.1 / Wolverine 6.1.0 on JasperFx 2.0). +Research was performed inline against the restored 9.2.1 / 6.1.0 assemblies +(reflection) and the JasperFx primary sources during the PR #125 work, rather +than via the usual prompt→artifact handoff; this artifact records the verified +findings so the rationale survives. + +## 1. Version set shipped + +| Package | From | To | +|---|---|---| +| Marten / Marten.EntityFrameworkCore | 8.37.1 | 9.2.1 | +| WolverineFx / .EntityFrameworkCore / .Marten | 5.39.3 | 6.1.0 | +| WolverineFx.RuntimeCompilation | — | 6.1.0 (new, dev/test path) | + +JasperFx 2.2.0 / JasperFx.Events 2.2.0 / Weasel 9.0.1 resolve transitively. All +target `net10.0` (the 9/6 wave drops `net8.0`; RunCoach is on .NET 10). NuGet +restore is conflict-free; no JasperFx version pin was required. + +## 2. Breaking changes encountered + fixes (ground truth) + +Verified empirically — each surfaced as a real compile error or test-boot failure +and was fixed against the restored assemblies, not from docs alone. + +1. **`[Identity]` attribute relocated** `Marten.Schema` → `JasperFx` (Marten 9 no + longer defines its own; it honors the shared JasperFx attribute). Fix: + `using JasperFx;` in `IdempotencyMarker.cs`. +2. **Enum/exception namespace moves** (JasperFx 2.0 consolidation): + - `TenancyStyle` → `JasperFx.MultiTenancy` + - `TrackLevel` → `JasperFx.OpenTelemetry` + - `DocumentAlreadyExistsException` → `JasperFx` (out of `Marten.Exceptions`; + the two stream-collision exceptions `ExistingStreamIdCollisionException` + and `ConcurrentUpdateException` stayed in `Marten.Exceptions`). +3. **Projection programming-model change** — convention-method + `SingleStreamProjection` subclasses (`OnboardingProjection`, + `PlanProjection`) must be declared `partial` so the compile-time + `JasperFx.Events.SourceGenerator` (shipped in the Marten NuGet analyzer asset) + emits the aggregate "Evolver" dispatcher. The runtime reflection fallback was + removed — without `partial` the store throws `InvalidProjectionException: No + source-generated dispatcher found` at first boot. `Apply`/`Create`/`ShouldDelete` + must be `public`. `EfCoreSingleStreamProjection` subclasses that override + `ApplyEvent` (e.g. `UserProfileFromOnboardingProjection`) use the explicit + virtual path and do NOT need `partial`. +4. **Wolverine 6 extracted runtime Roslyn codegen** into the opt-in + `WolverineFx.RuntimeCompilation` package. Any host booting with + `CodeGeneration.TypeLoadMode.Auto` (RunCoach's dev + integration-test host) + throws `No IAssemblyGenerator is registered` without it. Production uses + `TypeLoadMode.Static` (pre-generated) and ships without Roslyn — the intended + design. The package auto-registers `IAssemblyGenerator` via its module. + +**Verified unchanged (migrate as-is):** the reflection helper resolving the +assembly-root `EventStoreOptionsExtensions.MapEventTypeWithSchemaVersion(IEventStoreOptions, uint)` +(byte-identical signature in 9.2.1); `StreamIdentity.AsGuid`; `TenancyStyle.Conjoined`; +`EfCoreSingleStreamProjection` materializing an EF row in the Marten transaction; +`AddAsyncDaemon(DaemonMode.Solo)` ↔ `DurabilityMode.Solo`; `ApplyAllDatabaseChangesOnStartup`; +per-event schema-version tagging + `Events.Upcast` upcaster; +`DeleteAllTenantDataAsync` (GDPR); `IntegrateWithWolverine()`-alone envelope wiring +(DEC-048); the `TrackConnections` / `TrackEventCounters` OTel surface. + +## 3. Behavioral / default changes (no code break, but relevant) + +- **`EventAppendMode` default flipped** `Rich` → `QuickWithServerTimestamps`. We + set `Rich` explicitly, so the flip is inert today — but `Rich` is now + load-bearing rather than coincidental. Switching to QuickAppend is a deferred + throughput lever (see DEC-071 / ROADMAP). +- **`UseIdentityMapForAggregates` default flipped** `false` → `true`. We set + `true` explicitly; no change. +- **`EnableAdvancedAsyncTracking` default ON** — records high-water skips in a new + `mt_high_water_skips` table (created cleanly by `ApplyAllDatabaseChangesOnStartup`). + Improves Solo-daemon catch-up; verified the boot is clean. +- **DI defaults**: lightweight sessions + System.Text.Json are now the defaults + (Newtonsoft moved to `Marten.Newtonsoft`). We already use lightweight sessions + and STJ, so no change. `RestoreV8Defaults()` was deliberately NOT used. +- **Wolverine `ServiceLocationPolicy` default** flipped `AllowedButWarn` → + `NotAllowed`. Our constructor-injection DI is unaffected (no service-location + fallback in handlers). + +## 4. What the upgrade gets RunCoach + +**Directly useful now:** source-generated projection dispatch (faster apply, the +change that forced `partial`); `EnableAdvancedAsyncTracking` for better Solo-daemon +catch-up; PostgreSQL LISTEN/NOTIFY async-daemon wakeup (lower projection latency); +Roslyn removed from the hot path + AOT-clean `TypeLoadMode.Static` (leaner/faster +prod cold start); BigInt events (removes the ~2.1B-event ceiling, auto-migrates); +and the Wolverine 6.1 EF-outbox flush-timing correctness fix (the outbox flush +completes before the HTTP response is written — touches our "EF write + Marten +append in one TX" pattern; included in our 6.1.0 pin). + +**Available levers, not adopted:** QuickAppend append mode (~50% append +throughput); `Marten.PgVector` (9.3) embeddings-in-Postgres + `VectorProjection` +for the LLM/coaching layer; DCB tag-based cross-stream invariants; per-event binary +serialization (`Marten.MemoryPack`). A future patch bump to Wolverine 6.2.x would +add outgoing-envelope pooling (~90% fewer publish-path allocations) and the 6.2.2 +EF transaction-middleware codegen fix — neither is in our 6.1.0 pin. + +**Plumbing:** JasperFx 2.0 / JasperFx.Events 2.0 / Weasel 9.0 extraction; Lamar +removed (Wolverine uses MS DI fully); coordinated namespace moves. + +## 5. Verification + +`dotnet build RunCoach.slnx` clean (0 warnings under `TreatWarningsAsErrors` with +SonarAnalyzer 10.27 + StyleCop). Full suite **1124/1124 passing** +(`dotnet test --solution RunCoach.slnx`) on Testcontainers Postgres, including the +Solo async daemon + `ApplyAllDatabaseChangesOnStartup` clean boot, the legacy-event +upcaster synthetic-row regression test, and the idempotency Wolverine error-routing +tests. CI green on PR #125 including `Backend (build + test)` and the OpenAPI +codegen drift gate (confirming zero API-contract drift). + +## 6. Sources + +- JasperFx release announcement (2026-05-24) — `jeremydmiller.com/2026/05/24/marten-9-0-polecat-4-0-and-wolverine-9-0-are-live/` +- Wolverine migration guide — `wolverinefx.net/guide/migration`; codegen — `wolverinefx.net/guide/codegen` +- Marten releases — `github.com/JasperFx/marten/releases` (9.0.0–9.3); Wolverine releases — `github.com/JasperFx/wolverine/releases` (6.0.0–6.2.2) +- Wolverine 6.0 release punchlist — `github.com/JasperFx/wolverine/issues/2745` +- Verified in-repo against the restored `Marten 9.2.1` / `WolverineFx 6.1.0` / `JasperFx 2.2.0` assemblies. + +## Related + +- DEC-071 (this upgrade), DEC-048 / DEC-049 (Marten+Wolverine startup composition), DEC-067 (event upcasting — verified intact). diff --git a/docs/research/research-queue.md b/docs/research/research-queue.md index a284b924..1ab22f39 100644 --- a/docs/research/research-queue.md +++ b/docs/research/research-queue.md @@ -231,6 +231,10 @@ Research is batched by dependency. Later batches depend on findings from earlier - **Batch 26:** R-075 — single-artifact pre-spec pass landed 2026-05-18, Status `Integrated`. Resolved the shadcn/ui × Tailwind v4 × Catppuccin-hybrid-token × dark-mode wiring for sub-project 2a. **Verdict:** single `src/index.css` in shadcn's v4 canonical order (`tailwindcss` → `tw-animate-css` → `@custom-variant dark` → primitive tier → semantic tier → `@theme inline`); two-tier tokens (primitive `--ctp-*` Catppuccin Latte/Mocha below shadcn's semantic tokens, dark mode swaps the primitive tier only); Catppuccin hex pasted inline (not the `@catppuccin/tailwindcss` package); class-based dark mode via a ~40-LOC `ThemeProvider` + inline no-flash script, `defaultTheme="system"` + a 3-state Settings toggle; the contrast rule encoded in the token mappings (text roles → `text`/`subtext1` only — Latte `subtext0`-on-`base` is 4.37:1, an AA-normal fail); accent = one statically-committed OKLCH shade per mode (hue held, lightness moved); component set `button input label form card collapsible dialog sonner badge radio-group scroll-area` (~32–35 kB gz); `tw-animate-css` (CSS-only, consistent with DEC-063). Integrated as **DEC-070**; feeds the Slice 2a spec. +### Batch 27 (Critter Stack 2026 dependency upgrade — Marten 9 / Wolverine 6) + +- **Batch 27:** R-076 — Critter Stack upgrade migration map; landed 2026-05-30, Status `Integrated`. Marten 8.37.1→9.2.1 + Wolverine 5.39.3→6.1.0 on JasperFx 2.0. Captures the verified breaking changes (namespace moves of `[Identity]` / `TenancyStyle` / `TrackLevel` / `DocumentAlreadyExistsException` into the shared `JasperFx` assembly; convention-method `SingleStreamProjection` subclasses must be `partial` for the compile-time source-gen dispatcher; `WolverineFx.RuntimeCompilation` required for the dev/test `TypeLoadMode.Auto` host after Wolverine 6 pulled Roslyn out of core), the behavioral default flips (append mode → QuickAppend — kept `Rich`; advanced async tracking on), and the value the upgrade unlocks. Artifact: `batch-27a-critter-stack-9-6-migration.md`. Integrated as **DEC-071**. + --- ## Errata