# Project Conventions

> Core conventions and patterns for SentenceStudio

- Skill: `davidortinau/project-conventions` (Agent Skill)
- Install (CLI): `npx skillmds@latest add davidortinau/project-conventions`
- Raw SKILL.md: https://api.skillmd.com/api/skills/davidortinau/project-conventions/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: davidortinau (https://skillmd.com/u/davidortinau)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/davidortinau/project-conventions

---


## Context

SentenceStudio is a .NET MAUI Blazor Hybrid language learning app. .NET 10 SDK (10.0.101), multi-platform (iOS, Android, Mac Catalyst), with .NET Aspire orchestration.

## Project Structure

| Project | Purpose | Build |
|---------|---------|-------|
| `SentenceStudio.Shared` | Core: DbContext, repos, services, models, migrations | Multi-TFM |
| `SentenceStudio.AppLib` | Blazor Hybrid library, Scriban templates | Multi-TFM |
| `SentenceStudio.UI` | Blazor RCL — web pages, components | `dotnet build` (no -f) |
| `SentenceStudio.MacCatalyst/iOS/Android` | MAUI app heads | `-f net10.0-{platform}` |
| `SentenceStudio.Api` | ASP.NET Core backend | net10.0 |
| `SentenceStudio.WebApp` | Blazor Server frontend | net10.0 |
| `SentenceStudio.AppHost` | Aspire orchestration | net10.0 |
| `SentenceStudio.Infrastructure` | Server-side persistence (ServerDbContext) | net10.0 |
| `SentenceStudio.Workers` | Background services | net10.0 |

## Build Commands

```bash
# MAUI (NEVER use dotnet run)
dotnet build -f net10.0-maccatalyst
dotnet build -t:Run -f net10.0-maccatalyst

# UI project (no TFM flag)
dotnet build src/SentenceStudio.UI/SentenceStudio.UI.csproj

# Tests
dotnet test
```

## Testing

- **Framework:** xUnit + Moq + FluentAssertions (Unit), AutoFixture (Integration), AspNetCore.Mvc.Testing (API)
- **Projects:** `tests/SentenceStudio.UnitTests`, `tests/SentenceStudio.IntegrationTests`, `tests/SentenceStudio.Api.Tests`
- **Target:** net10.0

## Blazor Page Patterns

- Bootstrap icons only: `<i class="bi bi-{name}"></i>` — **NEVER emojis**
- Layout: `<PageHeader>` → `<ToolbarActions>` → main content with `card card-ss`
- Spinners: `<span class="spinner-border spinner-border-sm">`
- Alerts: `<div class="alert alert-{danger|warning|success}">`
- Auth: `@attribute [Authorize]` on protected pages
- Cleanup: `@implements IAsyncDisposable`

## Database Patterns

- **DbContext:** `ApplicationDbContext` — SQLite on mobile, PostgreSQL on server
- **Table names:** SINGULAR (configured in OnModelCreating)
- **Synced entities:** String GUID PKs with `ValueGeneratedNever()`
- **Non-synced:** Int auto-increment PKs
- **Migrations:** Always via `dotnet ef migrations add` — NEVER hand-write, NEVER raw SQL ALTER TABLE
- **Data isolation:** Filter by `UserProfileId`
- **CRITICAL:** Never call `EnsureCreatedAsync` before `MigrateAsync`
- **Migration location:** `Migrations/` (PostgreSQL), `Migrations/Sqlite/` (mobile)

## AI/Prompt Patterns

- **Templates:** 24+ Scriban templates in `src/SentenceStudio.AppLib/Resources/Raw/*.scriban-txt`
- **Client:** `IChatClient` (Microsoft.Extensions.AI) via `AiService.SendPrompt<T>()`
- **DTOs:** Use `[Description]` attributes on properties — no `[JsonPropertyName]`
- **Connectivity:** Check `_connectivity.IsInternetAvailable` before AI calls
- **Gateway:** Optional `IAiGatewayClient` for server routing

## Service Patterns

- Constructor injection via `IServiceProvider`
- All methods async (`Task<T>`)
- `ILogger<T>` for structured logging
- `WeakReferenceMessenger.Default.Send()` for cross-component messaging
- Scoped DbContext access: `_serviceProvider.CreateScope()` → `GetRequiredService<ApplicationDbContext>()`

## Error Handling

- Structured logging: `_logger.LogError(ex, "context {Field}", value)`
- `InvalidOperationException` for state violations
- `ArgumentException` for invalid arguments
- Default/null returns for external API failures (TTS, image)
- Connectivity resilience via `ConnectivityChangedMessage`

## Activity tracking (ad-hoc + plan items)

- Every activity page calls `ActivityTimer.StartSession(activityType, PlanItemId, resourceId, skillId)` **unconditionally** — when `PlanItemId` is null/empty the service creates a synthetic `DailyPlanCompletion` with `PlanItemId = "adhoc-{guid}"` so freeform sessions get duration tracking.
- `activityType` string must parse to a `PlanActivityType` enum value or the ad-hoc row is silently dropped. **Valid enum values** (see `IProgressService.cs`): `VocabularyReview, Reading, Listening, VideoWatching, Shadowing, Cloze, Translation, Writing, SceneDescription, Conversation, VocabularyGame`. **NOT valid**: `VocabularyMatching` (use `VocabularyGame`), `HowDoYouSay`, `WordAssociation`, `MinimalPairs`.
- `ReconstructPlanFromDatabase` filters `adhoc-*` so they don't show up in "Today's Plan"; `GetActivityLogAsync` includes them so day-detail shows freeform practice with its own "Freeform practice" cluster.
- Query param naming is NOT uniform: most pages use singular `ResourceIdParam`; `VocabQuiz`/`VocabMatching` use plural `ResourceIdsParam` (comma-separated — take `.Split(',').FirstOrDefault()` for ad-hoc persistence).

## Scriban prompt loops

Default iteration limit is 1000. Dynamic learning resources ("New Words") can return thousands of unpracticed terms. Any service rendering `{{ for t in terms }}` must cap the collection before passing to the template — current cap is **40 random words** in `TranslationService` and `ClozureService`. If adding a new activity that loops vocab, apply the same cap.

## MauiReactor Conventions

- Use `VStart()` / `VEnd()` not `Top()` / `Bottom()`
- Use `HStart()` / `HEnd()` not `Start()` / `End()`
- NEVER use `FillAndExpand` — legacy pattern
- NEVER put CollectionView inside scrollable containers

## Anti-Patterns (CRITICAL)

- ❌ NEVER uninstall/reinstall apps (destroys user data)
- ❌ NEVER delete database files without permission + backup
- ❌ NEVER use `dotnet run` for MAUI apps
- ❌ NEVER use emoji in UI/code/logs
- ❌ NEVER use raw SQL ALTER TABLE — always EF migrations
- ❌ NEVER suppress PendingModelChangesWarning
- ❌ NEVER put CollectionView inside VStack/scrollable containers
- ❌ NEVER use inline FontImageSource — define in ApplicationTheme.Icons.cs
- ❌ All documentation files go in `/docs/` not repo root

