Skip to content

Latest commit

 

History

History
256 lines (223 loc) · 13.7 KB

File metadata and controls

256 lines (223 loc) · 13.7 KB

Turbo Cloud AI Contract

This file is the canonical coding contract for AI-assisted changes in turbo-cloud. Tool-specific instruction files should reference this file instead of duplicating rules.

Foundational context

This repository targets the following core stack. When coding, prefer patterns compatible with these versions:

  • .NET SDK 9.0.310 (from global.json)
  • C# / BCL net9.0
  • Orleans 9.2.1
  • EF Core 9.0.8
  • Pomelo MySQL provider 9.0.0
  • SuperSocket 2.0.2

Skills activation

Activate the relevant skill checklist before editing code in that domain:

  • handler-development
    • Trigger: editing files under Turbo.PacketHandlers/** or message-to-composer orchestration logic.
    • Enforce: orchestration-only handlers, no DB queries, canonical grain access, no silent catches.
  • grain-development
    • Trigger: editing files under Turbo.*\\Grains\\** or Turbo.Primitives/**/Grains/*.cs.
    • Enforce: keep ownership boundaries, lifecycle rules, and snapshot/state coherence.
    • Enforce: all rules in the Orleans grain development rules section below.
  • session-presence-routing
    • Trigger: touching session gateway, presence flow, room routing, outbound composer fan-out.
    • Enforce: player outbound via PlayerPresenceGrain.SendComposerAsync; no direct handler socket sends.
  • message-contracts
    • Trigger: editing Turbo.Primitives/Messages/Incoming/** or outgoing composer payload mappings.
    • Enforce: explicit mandatory fields, no placeholder payloads when source data exists.
  • revision-protocol
    • Trigger: changes referencing Revision<id> packet mappings.
    • Enforce: edit Turbo.Revisions/Revision<id>/** in turbo-cloud.

Priority order

  1. Build and quality checks in repo files (Directory.Build.props, Directory.Build.targets, .editorconfig)
  2. CONTEXT.md architecture and placement boundaries
  3. Existing neighboring code conventions in the target folder
  4. Tool-specific adapters (for example .github/copilot-instructions.md)

Portable prompt contract

Use this request shape with any AI tool:

  1. Goal:
  2. Target files:
  3. Required context files:
  4. Invariants to preserve:
  5. Forbidden changes:
  6. Validation commands:
  7. Output format:

Default output format:

  • concise rationale
  • file-by-file diff summary
  • risks/assumptions
  • exact validation command results

Required standards

  • Target framework/tooling: .NET 9 pinned via global.json.
  • Keep C# formatting compatible with repo quality gates (dotnet csharpier check, dotnet format).
  • Follow .editorconfig naming/style preferences.
  • Keep diffs focused and minimal; avoid unrelated refactors.
  • Avoid introducing new dependencies unless required by the task.

Behavioral rules for generated code

  • Match local conventions in the files you touch.
  • Prefer deterministic handlers/services with clear guard clauses.
  • Preserve cancellation and async flow where it already exists.
  • Handle failure paths explicitly; do not ship happy-path-only changes.
  • Avoid dead code, unused allocations, and broad catch blocks that hide errors (see Orleans grain development rules for specifics).
  • For revision compatibility work, prefer restoring/adding missing incoming message contracts in Turbo.Primitives/Messages/Incoming/** before mutating serializer/composer payload behavior.
  • Do not alter serializer/composer behavior by replacing real payload writes with placeholder constants (for example, unconditional WriteInteger(0)) unless explicitly requested.
  • If work references Revision<id> parsers/serializers, edit Turbo.Revisions/Revision<id>/** in turbo-cloud.

Orleans grain development rules

These rules exist because every one of these mistakes has shipped and caused real issues.

Never swallow exceptions silently

Every bare catch { } hides a real bug path. Always use catch (Exception ex) and log it. If a cross-grain notification fails silently, state goes asymmetric and nobody knows why.

  • Required: inject ILogger<T> into every grain that does cross-grain calls or DB work.
  • Forbidden: bare catch { }, catch (Exception) { } without logging.

Parallelize independent grain calls

When checking status on N grains (e.g. online status for a friend list), do not await each one in a foreach. Grain calls to different grains can run concurrently with Task.WhenAll.

  • Sequential = O(n) round-trips. Parallel = O(1) wall time.
  • Apply everywhere: activation hydration, search results, batch accept/deny.

Do not repeat identical grain calls in loops

If a grain method calls its own player's GetSummaryAsync inside a loop, hoist the call before the loop. Same result every iteration = wasted round-trips.

Batch DB operations

Do not loop ExecuteDeleteAsync per entity. Use a single WHERE ... IN (...) query. Same for composer fan-out: collect all updates, send once.

Use timer-based flush for housekeeping writes

Follow the RoomPersistenceGrain pattern: queue dirty state, flush with RegisterGrainTimer on interval, and flush on OnDeactivateAsync. Do not issue per-event DB writes that block the grain turn.

Do not hardcode limits in grains

Handlers already read configuration values (e.g. Turbo:FriendList:UserFriendLimit) from IConfiguration and pass them to grains. Magic numbers like Take(50), Take(20), or maxIgnoreCapacity = 100 must come from configuration parameters on the grain interface method. A 10,000 user hotel needs different tuning than a 10 player dev server.

Use tracked deletes for atomicity

ExecuteDeleteAsync commits immediately and bypasses the EF change tracker. If a delete + insert must succeed or fail together, use FirstOrDefaultAsync + Remove so both go through one SaveChangesAsync.

Replace .Ignore() with a LogAndForget helper

Orleans .Ignore() makes cross-grain failures invisible. Use a LogAndForget extension that calls ContinueWith(OnlyOnFaulted) to log the exception. Still fire-and-forget, but failures are visible in production logs.

Bound session/history collections

Any in-memory collection that grows per-message (e.g. conversation history) must have a configurable cap. Without a cap, long-running sessions leak memory.

One grain per responsibility — isolate heavy I/O

Each major domain component should operate in its own grain. When a grain needs heavy I/O (DB writes, persistence flushes), delegate that work to a dedicated secondary grain so it does not block the primary grain's turn.

  • Example: RoomGrain delegates furniture saves to RoomPersistenceGrain. The room grain stays responsive while persistence queues and flushes.
  • Do not combine domain logic and persistence flushing in the same grain.

Use grain boundaries for thread safety

Orleans grains are single-threaded by design. Use this for concurrency-sensitive operations by giving each user their own grain for the operation.

  • Example: each player gets a PurchaseGrain so catalog purchases are serialized per-player with no locks needed.
  • Example: limited-edition items should use a dedicated grain (e.g. LimitedItemGrain) so concurrent buyers are safely serialized.
  • Do not add manual locking (lock, SemaphoreSlim) inside grains — that fights the actor model.

Grains orchestrate their own outbound communication

When grain state changes (e.g. wallet balance updates), the grain itself sends the snapshot to PlayerPresenceGrain.SendComposerAsync. The caller that triggered the change does not pass or send the composer — the grain owns that responsibility.

  • Correct: handler calls grain.UpdateWalletAsync(...) → grain updates state → grain calls PlayerPresenceGrain.SendComposerAsync(...).
  • Wrong: handler calls grain.UpdateWalletAsync(...) → handler builds composer → handler sends composer to player.

Do not mutate the database directly for grain-owned state

Grains may hold cached or in-memory state that will not reflect direct DB changes. All mutations to grain-owned data must go through the grain's methods, even when the player is offline.

  • If a grain uses [PersistentState], state is hydrated from the configured store (not DB) on activation. Direct DB edits will be overwritten by stale store data.
  • Admin tools and external systems must call grain methods, not issue raw SQL/DB updates, for data that grains own.

Profile and grain flow constraints

  • Keep packet handlers orchestration-only:
    • validate input
    • call grains through canonical grain-factory access patterns
    • map snapshot data to outgoing composers
  • Do not query database contexts or repositories directly from packet handlers.
  • Keep persistence access in grains/services/providers that own domain state.
  • Do not use ad-hoc grain key strings in handlers when extension-based access exists.
  • Do not add silent catch blocks in handlers.
  • When snapshot fields are available, map them into composer payloads instead of TODO placeholders.
  • For incoming message records in Turbo.Primitives/Messages/Incoming/**, keep mandatory fields explicit (use required where appropriate) instead of default-fallback contracts.

Session and room routing constraints

  • Connection/session lifecycle starts in gateway flow; do not duplicate session registration in handlers.
  • Post-SSO session attachment goes through PlayerPresenceGrain (one active presence grain per player id).
  • Player-targeted outbound flow must be:
    • resolve player presence grain
    • call SendComposerAsync
    • rely on presence fan-out to subscribed sessions
  • Do not send directly to raw sockets/session transports from packet handlers.
  • Active-room membership/discovery belongs to RoomDirectoryGrain; do not bypass it with ad-hoc room tracking.
  • Grain lifetime remains Orleans-managed by default; use [KeepAlive] only for explicitly justified directory/manager grains.

Packet addition checklist (revision work)

When adding packet mappings in Turbo.Revisions/Revision20260112:

  1. Update Turbo.Revisions/Revision20260112/Headers.cs:
    • add/update incoming MessageEvent id constants
    • add/update outgoing MessageComposer id constants
  2. Add parser class under:
    • Turbo.Revisions/Revision20260112/Parsers/<Domain>/*MessageParser.cs
  3. Add serializer class under:
    • Turbo.Revisions/Revision20260112/Serializers/<Domain>/*MessageComposerSerializer.cs
  4. Register mappings in:
    • Turbo.Revisions/Revision20260112/Revision20260112.cs
    • incoming: Parsers dictionary with MessageEvent key
    • outgoing: Serializers dictionary with composer type + MessageComposer id
  5. Ensure the required using directives are present in Revision20260112.cs for new parser/serializer namespaces.

Task recipes

Add packet handler

  • Required context files:
    • AGENTS.md
    • CONTEXT.md
    • Turbo.Primitives/Orleans/GrainFactoryExtensions.cs
  • Required references:
    • one handler in same domain under Turbo.PacketHandlers/<Domain>/
    • related incoming message type under Turbo.Primitives/Messages/Incoming/**
  • Forbidden changes:
    • no direct DB access in handler
    • no direct session/socket sends
    • no ad-hoc grain key literals when extension methods exist
  • Validation:
    • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheck
    • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate

Change grain behavior

  • Required context files:
    • AGENTS.md
    • CONTEXT.md
    • target grain interface in Turbo.Primitives/**/Grains/*.cs
  • Required references:
    • existing grain in same module
    • related snapshot/state types in Turbo.Primitives/Orleans/Snapshots/** or States/**
  • Forbidden changes:
    • no handler-layer fallback logic that bypasses grain ownership
    • no lifecycle changes that abuse [KeepAlive] without infrastructure justification
  • Validation:
    • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheck
    • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate

Add message/composer mapping

  • Required context files:
    • AGENTS.md
    • CONTEXT.md
    • neighboring message/composer classes
  • Required references:
    • incoming message under Turbo.Primitives/Messages/Incoming/**
    • outgoing composer under Turbo.Primitives/Messages/Outgoing/**
    • handler using same message family
  • Forbidden changes:
    • no placeholder payload collections when source snapshot data exists
    • no implicit default-fallback contracts for mandatory incoming fields
  • Validation:
    • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheck
    • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate

Refactor lookup/cache logic

  • Required context files:
    • AGENTS.md
    • CONTEXT.md
    • current lookup owner grain/service
  • Required references:
    • existing set/invalidate methods
    • all reverse-lookup call sites
  • Forbidden changes:
    • no one-way cache updates; forward/reverse mappings must stay coherent
    • no loss of case-insensitive semantics for username lookups
  • Validation:
    • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheck
    • dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate

Required validation before completion

dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudFastCheck
dotnet build Turbo.Main/Turbo.Main.csproj -t:TurboCloudQualityGate

Definition of done for AI changes

  • All modified files match nearby patterns and contract rules.
  • Quality gates pass with no new warnings introduced by the change.
  • Architecture invariants for touched areas are explicitly confirmed in PR.
  • Edge/failure behavior is addressed for logic changes.
  • Any context-rule updates needed by the change are included in the same PR.

PR expectations for AI-assisted work

  • Disclose AI usage and major generated sections.
  • Be able to explain complex generated logic in your own words.
  • Include verification of at least one edge/failure scenario when behavior changes.