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
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.*
124 changes: 124 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,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<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);
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).
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