|
1 | | -<!-- dgc-policy-v11 --> |
2 | | -# Dual-Graph Context Policy |
| 1 | +# Resgrid Project Guide |
3 | 2 |
|
4 | | -This project uses a local dual-graph MCP server for efficient context retrieval. |
| 3 | +## Overview |
5 | 4 |
|
6 | | -## MANDATORY: Adaptive graph_continue rule |
| 5 | +Resgrid is a logistics and resource management platform for emergency services (fire, EMS, SAR). It's a .NET (C#) monolith solution organized into 30+ projects across 7 areas. |
7 | 6 |
|
8 | | -**Call `graph_continue` ONLY when you do NOT already know the relevant files.** |
| 7 | +## Solution Structure |
9 | 8 |
|
10 | | -### Call `graph_continue` when: |
11 | | -- This is the first message of a new task / conversation |
12 | | -- The task shifts to a completely different area of the codebase |
13 | | -- You need files you haven't read yet in this session |
| 9 | +``` |
| 10 | +Resgrid.sln |
| 11 | +├── Web/ # ASP.NET web apps |
| 12 | +│ ├── Resgrid.Web/ # Main MVC web application |
| 13 | +│ ├── Resgrid.Web.Services/ # REST API (v4 controllers) |
| 14 | +│ ├── Resgrid.Web.Eventing/ # Webhook/event endpoint |
| 15 | +│ ├── Resgrid.Web.Mcp/ # MCP endpoint |
| 16 | +│ └── Resgrid.Web.Tts/ # Text-to-speech |
| 17 | +├── Core/ # Core business logic |
| 18 | +│ ├── Resgrid.Config/ # Static config classes (one per domain) |
| 19 | +│ ├── Resgrid.Framework/ # Utilities: Logging, Serialization, Hashing |
| 20 | +│ ├── Resgrid.Localization/ # Localization strings |
| 21 | +│ ├── Resgrid.Model/ # Entities, enums, interfaces (Services, Repositories, Providers) |
| 22 | +│ └── Resgrid.Services/ # Service implementations |
| 23 | +├── Repositories/ # Data access |
| 24 | +│ ├── Resgrid.Repositories.DataRepository/ # SQL Server / Dapper |
| 25 | +│ └── Resgrid.Repositories.NoSqlRepository/ # MongoDB |
| 26 | +├── Providers/ # Infrastructure implementations |
| 27 | +│ ├── Resgrid.Providers.Cache/ # Redis caching (AzureRedisCacheProvider) |
| 28 | +│ ├── Resgrid.Providers.Bus/ # Azure Service Bus |
| 29 | +│ ├── Resgrid.Providers.Bus.Rabbit/ # RabbitMQ alternative |
| 30 | +│ ├── Resgrid.Providers.Email/ # Email delivery |
| 31 | +│ ├── Resgrid.Providers.Geo/ # Geolocation |
| 32 | +│ ├── Resgrid.Providers.Marketing/ # Marketing/CRM |
| 33 | +│ ├── Resgrid.Providers.Messaging/ # Push notifications |
| 34 | +│ ├── Resgrid.Providers.Migrations/ # SQL Server migrations |
| 35 | +│ ├── Resgrid.Providers.MigrationsPg/# PostgreSQL migrations |
| 36 | +│ ├── Resgrid.Providers.Number/ # Phone number provisioning |
| 37 | +│ ├── Resgrid.Providers.Pdf/ # PDF generation |
| 38 | +│ ├── Resgrid.Providers.Voip/ # VoIP/SIP |
| 39 | +│ ├── Resgrid.Providers.Weather/ # Weather data |
| 40 | +│ ├── Resgrid.Providers.Workflow/ # Workflow execution |
| 41 | +│ ├── Resgrid.Providers.Claims/ # Custom auth claims |
| 42 | +│ └── Resgrid.Providers.AddressVerification/ |
| 43 | +├── Workers/ # Background job processing |
| 44 | +│ ├── Resgrid.Workers.Framework/ # Worker logic + Bootstrapper |
| 45 | +│ ├── Resgrid.Workers.Console/ # Worker host (console app) |
| 46 | +│ └── Support/Quidjibo.Postgres/ # Queue backend for PostgreSQL |
| 47 | +├── Tests/ # Test projects |
| 48 | +│ ├── Resgrid.Tests/ |
| 49 | +│ ├── Resgrid.SmokeTests/ |
| 50 | +│ └── Resgrid.Intergration.Tests/ |
| 51 | +└── Tools/ |
| 52 | + └── Resgrid.Console/ # Admin CLI tools |
| 53 | +``` |
| 54 | + |
| 55 | +## Build Configurations |
| 56 | + |
| 57 | +7 solution configurations: `Debug`, `Release`, `Docker`, `Azure`, `Cloud`, `Staging`, plus `x86`/`x64` variants. |
| 58 | + |
| 59 | +Build command: `dotnet build Resgrid.sln` |
14 | 60 |
|
15 | | -### SKIP `graph_continue` when: |
16 | | -- You already identified the relevant files earlier in this conversation |
17 | | -- You are doing follow-up work on files already read (verify, refactor, test, docs, cleanup, commit) |
18 | | -- The task is pure text (writing a commit message, summarising, explaining) |
| 61 | +The `Directory.Build.props` sets OS-conditional intermediate output paths: |
| 62 | +- Windows: `obj/windows/` |
| 63 | +- Linux/Unix: `obj/unix/` |
19 | 64 |
|
20 | | -**If skipping, go directly to `graph_read` on the already-known `file::symbol`.** |
| 65 | +## Architecture & Conventions |
21 | 66 |
|
22 | | -## When you DO call graph_continue |
| 67 | +### Layered Architecture |
23 | 68 |
|
24 | | -1. **If `graph_continue` returns `needs_project=true`**: call `graph_scan` with `pwd`. Do NOT ask the user. |
| 69 | +``` |
| 70 | +Config → Model → Services → Repositories/Providers → Web/Workers |
| 71 | +``` |
25 | 72 |
|
26 | | -2. **If `graph_continue` returns `skip=true`**: fewer than 5 files — read only specifically named files. |
| 73 | +Each layer depends only on the layer(s) to its left: |
| 74 | +- **Config** (`Resgrid.Config`): Static configuration classes, no dependencies |
| 75 | +- **Model** (`Resgrid.Model`): Entities, enums, interfaces — no external deps |
| 76 | +- **Services** (`Resgrid.Services`): Business logic — depends on Model |
| 77 | +- **Repositories** (`Resgrid.Repositories.*`): Data access — depends on Model |
| 78 | +- **Providers** (`Resgrid.Providers.*`): External integrations — depends on Model |
| 79 | +- **Web/Workers**: Entry points — depend on everything |
27 | 80 |
|
28 | | -3. **Read `recommended_files`** using `graph_read`. |
29 | | - - Always use `file::symbol` notation (e.g. `src/auth.ts::handleLogin`) — never read whole files. |
30 | | - - `recommended_files` entries that already contain `::` must be passed verbatim. |
| 81 | +### Dependency Injection (Autofac + Service Locator) |
31 | 82 |
|
32 | | -4. **Obey confidence caps:** |
33 | | - - `confidence=high` → Stop. Do NOT grep or explore further. |
34 | | - - `confidence=medium` → `fallback_rg` at most `max_supplementary_greps` times, then `graph_read` at most `max_supplementary_files` more symbols. Stop. |
35 | | - - `confidence=low` → same as medium. Stop. |
| 83 | +This codebase uses **Service Locator** pattern, NOT constructor injection: |
36 | 84 |
|
37 | | -## Session State (compact, update after every turn) |
| 85 | +```csharp |
| 86 | +// How services are resolved throughout the codebase: |
| 87 | +var service = Bootstrapper.GetKernel().Resolve<ISomeService>(); |
| 88 | +``` |
| 89 | + |
| 90 | +The `Bootstrapper` class (in `Resgrid.Workers.Framework/Bootstrapper.cs`) initializes Autofac with module-based registration: |
| 91 | +```csharp |
| 92 | +var builder = new ContainerBuilder(); |
| 93 | +builder.RegisterModule(new DataModule()); |
| 94 | +builder.RegisterModule(new ServicesModule()); |
| 95 | +builder.RegisterModule(new CacheProviderModule()); |
| 96 | +// ... more modules |
| 97 | +_container = builder.Build(); |
| 98 | +``` |
| 99 | + |
| 100 | +**When adding new services, you MUST update the Autofac module files** (typically `DataModule.cs` or `ServicesModule.cs`) to register your new type against its interface. |
| 101 | + |
| 102 | +### Configuration System |
| 103 | + |
| 104 | +Configuration is NOT in `appsettings.json`. It uses **static classes with mutable fields** loaded via reflection: |
| 105 | + |
| 106 | +1. Individual static classes in `Core/Resgrid.Config/` — one per domain (e.g., `SystemBehaviorConfig`, `CacheConfig`, `ApiConfig`) |
| 107 | +2. All config fields are `public static` (NOT properties with getters/setters) |
| 108 | +3. `ConfigProcessor.LoadAndProcessConfig()` uses reflection to find classes in the `Resgrid.Config` namespace and set their static fields |
| 109 | +4. Values come from a JSON file (keyed as `"ClassName.FieldName"`) or environment variables (keyed as `RESGRID:ClassName:FieldName`) |
| 110 | + |
| 111 | +**Usage:** `Config.SystemBehaviorConfig.CacheEnabled`, `Config.CacheConfig.RedisConnectionString` |
| 112 | + |
| 113 | +### Caching (Redis Cache-Aside) |
| 114 | + |
| 115 | +All caching goes through `ICacheProvider` — implemented by `AzureRedisCacheProvider`. |
| 116 | + |
| 117 | +**Key method used everywhere:** |
| 118 | +```csharp |
| 119 | +T Retrieve<T>(string cacheKey, Func<T> fallbackFunction, TimeSpan expiration) |
| 120 | +Task<T> RetrieveAsync<T>(string cacheKey, Func<Task<T>> fallbackFunction, TimeSpan expiration) |
| 121 | +``` |
38 | 122 |
|
39 | | -Maintain a short JSON block in your working memory. Update it after each turn: |
| 123 | +**Cache-Aside Pattern:** Try cache → on miss call fallback → store result → return. Cache keys are environment-prefixed (e.g., `DEV_`, `QA_`, `ST_`) based on `SystemBehaviorConfig.Environment`. |
40 | 124 |
|
41 | | -```json |
| 125 | +**Common pattern in Services** (local function + cache wrapper): |
| 126 | +```csharp |
| 127 | +public async Task<Foo> GetFooAsync(int departmentId, bool bypassCache = false) |
42 | 128 | { |
43 | | - "files_identified": ["path/to/file.py"], |
44 | | - "symbols_changed": ["module::function"], |
45 | | - "fix_applied": true, |
46 | | - "features_added": ["description"], |
47 | | - "open_issues": ["one-line note"] |
| 129 | + async Task<Foo> getFoo() |
| 130 | + { |
| 131 | + // ... actual logic ... |
| 132 | + return foo; |
| 133 | + } |
| 134 | + |
| 135 | + if (!bypassCache && Config.SystemBehaviorConfig.CacheEnabled) |
| 136 | + return await _cacheProvider.RetrieveAsync<Foo>(cacheKey, getFoo, cacheDuration); |
| 137 | + else |
| 138 | + return await getFoo(); |
48 | 139 | } |
49 | 140 | ``` |
50 | 141 |
|
51 | | -Use this state — not prose summaries — to remember what's been done across turns. |
| 142 | +**IMPORTANT:** The `bypassCache` parameter defaults to `false`. Many production callers do NOT bypass cache, so changes may not take effect for up to the cache duration (commonly 14 days for plan limits, 1 day for general data). Call `Invalidate*Cache` methods or set `bypassCache: true` when testing. |
52 | 143 |
|
53 | | -## Token Usage |
| 144 | +### Logging |
54 | 145 |
|
55 | | -A `token-counter` MCP is available for tracking live token usage. |
| 146 | +```csharp |
| 147 | +Resgrid.Framework.Logging.LogException(Exception ex, string extraMessage = null, string correlationId = null) |
| 148 | +Resgrid.Framework.Logging.LogError(string message) |
| 149 | +Resgrid.Framework.Logging.LogInfo(string message) |
| 150 | +Resgrid.Framework.Logging.LogDebug(string message) |
| 151 | +``` |
56 | 152 |
|
57 | | -- Before reading a large file: `count_tokens({text: "<content>"})` to check cost first. |
58 | | -- To show running session cost: `get_session_stats()` |
59 | | -- To log completed task: `log_usage({input_tokens: N, output_tokens: N, description: "task"})` |
| 153 | +Uses Serilog under the hood with optional Sentry integration. `LogException` automatically captures `[CallerFilePath]`, `[CallerMemberName]`, `[CallerLineNumber]`. |
60 | 154 |
|
61 | | -## Rules |
| 155 | +### Naming Conventions |
62 | 156 |
|
63 | | -- Do NOT use `rg`, `grep`, or bash file exploration before calling `graph_continue` (when required). |
64 | | -- Do NOT do broad/recursive exploration at any confidence level. |
65 | | -- `max_supplementary_greps` and `max_supplementary_files` are hard caps — never exceed them. |
66 | | -- Do NOT call `graph_continue` more than once per turn. |
67 | | -- Always use `file::symbol` notation with `graph_read` — never bare filenames. |
68 | | -- After edits, call `graph_register_edit` with changed files using `file::symbol` notation. |
| 157 | +| Layer | Interface | Implementation | Location | |
| 158 | +|---|---|---|---| |
| 159 | +| Services | `I{Name}Service` | `{Name}Service` | `Core/Resgrid.Services/` | |
| 160 | +| Repositories | `I{Name}Repository` | `{Name}Repository` | `Repositories/Resgrid.Repositories.DataRepository/` | |
| 161 | +| Providers | `I{Name}Provider` | `{Name}Provider` | `Providers/Resgrid.Providers.{Domain}/` | |
69 | 162 |
|
70 | | -## Context Store |
| 163 | +Service methods are almost all `async` returning `Task<T>`. Method naming: `{Verb}{Entity}{Filter}Async` (e.g., `GetAllUsersForDepartmentAsync`, `CreateUserState`). |
71 | 164 |
|
72 | | -Whenever you make a decision, identify a task, note a next step, fact, or blocker during a conversation, append it to `.dual-graph/context-store.json`. |
| 165 | +### Worker Pattern |
73 | 166 |
|
74 | | -**Entry format:** |
75 | | -```json |
76 | | -{"type": "decision|task|next|fact|blocker", "content": "one sentence max 15 words", "tags": ["topic"], "files": ["relevant/file.ts"], "date": "YYYY-MM-DD"} |
| 167 | +Workers follow a consistent pattern (`Workers/Resgrid.Workers.Framework/Logic/`): |
| 168 | +```csharp |
| 169 | +public async Task<Tuple<bool, string>> Process({Type}QueueItem item) |
| 170 | +{ |
| 171 | + try |
| 172 | + { |
| 173 | + // ... process item ... |
| 174 | + return new Tuple<bool, string>(true, ""); |
| 175 | + } |
| 176 | + catch (Exception ex) |
| 177 | + { |
| 178 | + Logging.LogException(ex); |
| 179 | + return new Tuple<bool, string>(false, ex.ToString()); |
| 180 | + } |
| 181 | +} |
77 | 182 | ``` |
78 | 183 |
|
79 | | -**To append:** Read the file → add the new entry to the array → Write it back → call `graph_register_edit` on `.dual-graph/context-store.json`. |
| 184 | +Task type discrimination uses `(int)TaskTypes.SomeEnum`. |
| 185 | + |
| 186 | +## Critical Gotchas & Common Bug Patterns |
| 187 | + |
| 188 | +### 1. Billing API Response Null Safety |
| 189 | + |
| 190 | +**`SubscriptionsService.GetCurrentPlanForDepartmentAsync()`** and **`GetPlanCountsForDepartmentAsync()`** call the external Billing API. Both check `response.Data == null` but the inner `response.Data.Data` can still be null when the API succeeds with an empty payload. Always null-check results from these methods. |
| 191 | + |
| 192 | +### 2. Null Plan from GetCurrentPlanForDepartmentAsync |
| 193 | + |
| 194 | +When Billing API is configured but returns a response where `Data.Data` is null, `GetCurrentPlanForDepartmentAsync` returns null instead of the free plan fallback. Callers that access `plan.PlanId` or `plan.GetLimitForTypeAsInt()` will NRE. |
| 195 | + |
| 196 | +### 3. Service Locator in Constructors |
| 197 | + |
| 198 | +Unlike modern DI, this codebase resolves dependencies explicitly in constructors via `Bootstrapper.GetKernel().Resolve<T>()`. When examining stack traces, dependencies are never null due to constructor injection failures — the Bootstrapper would fail at app start. If a NullReferenceException occurs on a service call, the issue is typically in the return value of the called method, not the service reference itself. |
80 | 199 |
|
81 | | -**Rules:** |
82 | | -- Only log things worth remembering across sessions (not every minor detail) |
83 | | -- `content` must be under 15 words |
84 | | -- `files` lists the files this decision/task relates to (can be empty) |
85 | | -- Log immediately when the item arises — not at session end |
| 200 | +### 4. Async State Machine Line Numbers |
86 | 201 |
|
87 | | -## Session End |
| 202 | +PDB line numbers in async stack traces can be off by 1-2 lines from the actual source. An NRE reported at the `await` line often actually occurs on the next line where the awaited result is used. |
88 | 203 |
|
89 | | -When the user signals they are done (e.g. "bye", "done", "wrap up", "end session"), proactively update `CONTEXT.md` in the project root with: |
90 | | -- **Current Task**: one sentence on what was being worked on |
91 | | -- **Key Decisions**: bullet list, max 3 items |
92 | | -- **Next Steps**: bullet list, max 3 items |
| 204 | +### 5. Cache Duration |
93 | 205 |
|
94 | | -Keep `CONTEXT.md` under 20 lines total. Do NOT summarize the full conversation — only what's needed to resume next session. |
| 206 | +Plan limits are cached for **14 days** (`TimeSpan.FromDays(14)`). Most user/department data is cached for **1 day**. Use `bypassCache: true` or call invalidation methods when you need fresh data. |
| 207 | + |
| 208 | +## Key File Index |
| 209 | + |
| 210 | +| Purpose | File | |
| 211 | +|---|---| |
| 212 | +| Solution file | `Resgrid.sln` | |
| 213 | +| Build props | `Directory.Build.props` | |
| 214 | +| DI Bootstrapper | `Workers/Resgrid.Workers.Framework/Bootstrapper.cs` | |
| 215 | +| Logging | `Core/Resgrid.Framework/Logging.cs` | |
| 216 | +| Config processor | `Core/Resgrid.Config/ConfigProcessor.cs` | |
| 217 | +| System behavior config | `Core/Resgrid.Config/SystemBehaviorConfig.cs` | |
| 218 | +| Cache config | `Core/Resgrid.Config/CacheConfig.cs` | |
| 219 | +| Redis cache provider | `Providers/Resgrid.Providers.Cache/AzureRedisCacheProvider.cs` | |
| 220 | +| Cache interface | `Core/Resgrid.Model/Providers/ICacheProvider.cs` | |
| 221 | +| Subscriptions (billing) | `Core/Resgrid.Services/SubscriptionsService.cs` | |
| 222 | +| Limits service | `Core/Resgrid.Services/LimitsService.cs` | |
| 223 | +| Departments service | `Core/Resgrid.Services/DepartmentsService.cs` | |
| 224 | +| Service interfaces | `Core/Resgrid.Model/Services/` (83 interfaces) | |
| 225 | +| Billing API DTOs | `Core/Resgrid.Model/Billing/Api/` | |
| 226 | +| Worker logic | `Workers/Resgrid.Workers.Framework/Logic/` | |
| 227 | +| Worker queue items | `Core/Resgrid.Model/Queue/` | |
| 228 | + |
| 229 | +## Common Tasks |
| 230 | + |
| 231 | +**Build the entire solution:** |
| 232 | +```bash |
| 233 | +dotnet build Resgrid.sln |
| 234 | +``` |
| 235 | + |
| 236 | +**Build a specific project:** |
| 237 | +```bash |
| 238 | +dotnet build Core/Resgrid.Services/Resgrid.Services.csproj |
| 239 | +``` |
| 240 | + |
| 241 | +**Find all implementations of an interface:** |
| 242 | +```bash |
| 243 | +grep -r "I{Name}Service" --include="*.cs" |
| 244 | +``` |
0 commit comments