Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 3 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
28 changes: 28 additions & 0 deletions docs/decisions/decision-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
122 changes: 122 additions & 0 deletions docs/research/artifacts/batch-27a-critter-stack-9-6-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# 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<TDoc,TId>` 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<T>(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<TOld,TNew>` 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);
Wolverine 6.1 EF-outbox flush-timing correctness fix and 6.2.2 EF transaction-
middleware codegen fix (both touch our "EF write + Marten append in one TX"
pattern); Wolverine 6.2 outgoing-envelope pooling (~90% fewer publish-path allocs).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

**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`).

**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).
4 changes: 4 additions & 0 deletions docs/research/research-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading