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
274 changes: 212 additions & 62 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,94 +1,244 @@
<!-- dgc-policy-v11 -->
# Dual-Graph Context Policy
# Resgrid Project Guide

This project uses a local dual-graph MCP server for efficient context retrieval.
## Overview

## MANDATORY: Adaptive graph_continue rule
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.

**Call `graph_continue` ONLY when you do NOT already know the relevant files.**
## Solution Structure

### Call `graph_continue` when:
- This is the first message of a new task / conversation
- The task shifts to a completely different area of the codebase
- You need files you haven't read yet in this session
```
Resgrid.sln
├── Web/ # ASP.NET web apps
│ ├── Resgrid.Web/ # Main MVC web application
│ ├── Resgrid.Web.Services/ # REST API (v4 controllers)
│ ├── Resgrid.Web.Eventing/ # Webhook/event endpoint
│ ├── Resgrid.Web.Mcp/ # MCP endpoint
│ └── Resgrid.Web.Tts/ # Text-to-speech
├── Core/ # Core business logic
│ ├── Resgrid.Config/ # Static config classes (one per domain)
│ ├── Resgrid.Framework/ # Utilities: Logging, Serialization, Hashing
│ ├── Resgrid.Localization/ # Localization strings
│ ├── Resgrid.Model/ # Entities, enums, interfaces (Services, Repositories, Providers)
│ └── Resgrid.Services/ # Service implementations
├── Repositories/ # Data access
│ ├── Resgrid.Repositories.DataRepository/ # SQL Server / Dapper
│ └── Resgrid.Repositories.NoSqlRepository/ # MongoDB
├── Providers/ # Infrastructure implementations
│ ├── Resgrid.Providers.Cache/ # Redis caching (AzureRedisCacheProvider)
│ ├── Resgrid.Providers.Bus/ # Azure Service Bus
│ ├── Resgrid.Providers.Bus.Rabbit/ # RabbitMQ alternative
│ ├── Resgrid.Providers.Email/ # Email delivery
│ ├── Resgrid.Providers.Geo/ # Geolocation
│ ├── Resgrid.Providers.Marketing/ # Marketing/CRM
│ ├── Resgrid.Providers.Messaging/ # Push notifications
│ ├── Resgrid.Providers.Migrations/ # SQL Server migrations
│ ├── Resgrid.Providers.MigrationsPg/# PostgreSQL migrations
│ ├── Resgrid.Providers.Number/ # Phone number provisioning
│ ├── Resgrid.Providers.Pdf/ # PDF generation
│ ├── Resgrid.Providers.Voip/ # VoIP/SIP
│ ├── Resgrid.Providers.Weather/ # Weather data
│ ├── Resgrid.Providers.Workflow/ # Workflow execution
│ ├── Resgrid.Providers.Claims/ # Custom auth claims
│ └── Resgrid.Providers.AddressVerification/
├── Workers/ # Background job processing
│ ├── Resgrid.Workers.Framework/ # Worker logic + Bootstrapper
│ ├── Resgrid.Workers.Console/ # Worker host (console app)
│ └── Support/Quidjibo.Postgres/ # Queue backend for PostgreSQL
├── Tests/ # Test projects
│ ├── Resgrid.Tests/
│ ├── Resgrid.SmokeTests/
│ └── Resgrid.Intergration.Tests/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix typo in test project name.

Resgrid.Intergration.Tests appears misspelled; should be Resgrid.Integration.Tests (assuming standard naming).

Suggested fix
-│   └── Resgrid.Intergration.Tests/
+│   └── Resgrid.Integration.Tests/
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
│ └── Resgrid.Intergration.Tests/
│ └── Resgrid.Integration.Tests/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` at line 50, Update the misspelled test project name entry in
CLAUDE.md: replace the string "Resgrid.Intergration.Tests/" with the correct
"Resgrid.Integration.Tests/" so the project name is spelled consistently (look
for the line containing Resgrid.Intergration.Tests/ in CLAUDE.md and correct the
typo).

└── Tools/
└── Resgrid.Console/ # Admin CLI tools
```
Comment on lines +9 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language identifier to this fenced code block.

This block triggers markdownlint MD040. Use text (or bash/plaintext as appropriate) after the opening fence.

Suggested fix
-```
+```text
 Resgrid.sln
 ├── Web/                          # ASP.NET web apps
 ...
 └── Tools/
     └── Resgrid.Console/          # Admin CLI tools
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 9 - 53, The fenced code block in CLAUDE.md is missing
a language identifier, triggering markdownlint MD040; update the opening
triple-backtick for the directory tree to include a language (e.g., add "text"
so it reads ```text) so the block is properly identified and the linter error is
resolved.


## Build Configurations

7 solution configurations: `Debug`, `Release`, `Docker`, `Azure`, `Cloud`, `Staging`, plus `x86`/`x64` variants.

Build command: `dotnet build Resgrid.sln`

### SKIP `graph_continue` when:
- You already identified the relevant files earlier in this conversation
- You are doing follow-up work on files already read (verify, refactor, test, docs, cleanup, commit)
- The task is pure text (writing a commit message, summarising, explaining)
The `Directory.Build.props` sets OS-conditional intermediate output paths:
- Windows: `obj/windows/`
- Linux/Unix: `obj/unix/`

**If skipping, go directly to `graph_read` on the already-known `file::symbol`.**
## Architecture & Conventions

## When you DO call graph_continue
### Layered Architecture

1. **If `graph_continue` returns `needs_project=true`**: call `graph_scan` with `pwd`. Do NOT ask the user.
```
Config → Model → Services → Repositories/Providers → Web/Workers
```
Comment on lines +69 to +71
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language identifier to this fenced code block.

This also triggers markdownlint MD040; set a language for the architecture diagram block.

Suggested fix
-```
+```text
 Config  →  Model  →  Services  →  Repositories/Providers  →  Web/Workers
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 69-69: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 69 - 71, Update the fenced code block that contains
the architecture line "Config  →  Model  →  Services  →  Repositories/Providers 
→  Web/Workers" to include a language identifier (e.g., change ``` to ```text)
so the block is flagged as plain text and satisfies markdownlint MD040.


2. **If `graph_continue` returns `skip=true`**: fewer than 5 files — read only specifically named files.
Each layer depends only on the layer(s) to its left:
- **Config** (`Resgrid.Config`): Static configuration classes, no dependencies
- **Model** (`Resgrid.Model`): Entities, enums, interfaces — no external deps
- **Services** (`Resgrid.Services`): Business logic — depends on Model
- **Repositories** (`Resgrid.Repositories.*`): Data access — depends on Model
- **Providers** (`Resgrid.Providers.*`): External integrations — depends on Model
- **Web/Workers**: Entry points — depend on everything

3. **Read `recommended_files`** using `graph_read`.
- Always use `file::symbol` notation (e.g. `src/auth.ts::handleLogin`) — never read whole files.
- `recommended_files` entries that already contain `::` must be passed verbatim.
### Dependency Injection (Autofac + Service Locator)

4. **Obey confidence caps:**
- `confidence=high` → Stop. Do NOT grep or explore further.
- `confidence=medium` → `fallback_rg` at most `max_supplementary_greps` times, then `graph_read` at most `max_supplementary_files` more symbols. Stop.
- `confidence=low` → same as medium. Stop.
This codebase uses **Service Locator** pattern, NOT constructor injection:

## Session State (compact, update after every turn)
```csharp
// How services are resolved throughout the codebase:
var service = Bootstrapper.GetKernel().Resolve<ISomeService>();
```

The `Bootstrapper` class (in `Resgrid.Workers.Framework/Bootstrapper.cs`) initializes Autofac with module-based registration:
```csharp
var builder = new ContainerBuilder();
builder.RegisterModule(new DataModule());
builder.RegisterModule(new ServicesModule());
builder.RegisterModule(new CacheProviderModule());
// ... more modules
_container = builder.Build();
```

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

### Configuration System

Configuration is NOT in `appsettings.json`. It uses **static classes with mutable fields** loaded via reflection:

1. Individual static classes in `Core/Resgrid.Config/` — one per domain (e.g., `SystemBehaviorConfig`, `CacheConfig`, `ApiConfig`)
2. All config fields are `public static` (NOT properties with getters/setters)
3. `ConfigProcessor.LoadAndProcessConfig()` uses reflection to find classes in the `Resgrid.Config` namespace and set their static fields
4. Values come from a JSON file (keyed as `"ClassName.FieldName"`) or environment variables (keyed as `RESGRID:ClassName:FieldName`)

**Usage:** `Config.SystemBehaviorConfig.CacheEnabled`, `Config.CacheConfig.RedisConnectionString`

### Caching (Redis Cache-Aside)

All caching goes through `ICacheProvider` — implemented by `AzureRedisCacheProvider`.

**Key method used everywhere:**
```csharp
T Retrieve<T>(string cacheKey, Func<T> fallbackFunction, TimeSpan expiration)
Task<T> RetrieveAsync<T>(string cacheKey, Func<Task<T>> fallbackFunction, TimeSpan expiration)
```

Maintain a short JSON block in your working memory. Update it after each turn:
**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`.

```json
**Common pattern in Services** (local function + cache wrapper):
```csharp
public async Task<Foo> GetFooAsync(int departmentId, bool bypassCache = false)
{
"files_identified": ["path/to/file.py"],
"symbols_changed": ["module::function"],
"fix_applied": true,
"features_added": ["description"],
"open_issues": ["one-line note"]
async Task<Foo> getFoo()
{
// ... actual logic ...
return foo;
}

if (!bypassCache && Config.SystemBehaviorConfig.CacheEnabled)
return await _cacheProvider.RetrieveAsync<Foo>(cacheKey, getFoo, cacheDuration);
else
return await getFoo();
}
```

Use this state — not prose summaries — to remember what's been done across turns.
**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.

## Token Usage
### Logging

A `token-counter` MCP is available for tracking live token usage.
```csharp
Resgrid.Framework.Logging.LogException(Exception ex, string extraMessage = null, string correlationId = null)
Resgrid.Framework.Logging.LogError(string message)
Resgrid.Framework.Logging.LogInfo(string message)
Resgrid.Framework.Logging.LogDebug(string message)
```

- Before reading a large file: `count_tokens({text: "<content>"})` to check cost first.
- To show running session cost: `get_session_stats()`
- To log completed task: `log_usage({input_tokens: N, output_tokens: N, description: "task"})`
Uses Serilog under the hood with optional Sentry integration. `LogException` automatically captures `[CallerFilePath]`, `[CallerMemberName]`, `[CallerLineNumber]`.

## Rules
### Naming Conventions

- Do NOT use `rg`, `grep`, or bash file exploration before calling `graph_continue` (when required).
- Do NOT do broad/recursive exploration at any confidence level.
- `max_supplementary_greps` and `max_supplementary_files` are hard caps — never exceed them.
- Do NOT call `graph_continue` more than once per turn.
- Always use `file::symbol` notation with `graph_read` — never bare filenames.
- After edits, call `graph_register_edit` with changed files using `file::symbol` notation.
| Layer | Interface | Implementation | Location |
|---|---|---|---|
| Services | `I{Name}Service` | `{Name}Service` | `Core/Resgrid.Services/` |
| Repositories | `I{Name}Repository` | `{Name}Repository` | `Repositories/Resgrid.Repositories.DataRepository/` |
| Providers | `I{Name}Provider` | `{Name}Provider` | `Providers/Resgrid.Providers.{Domain}/` |

## Context Store
Service methods are almost all `async` returning `Task<T>`. Method naming: `{Verb}{Entity}{Filter}Async` (e.g., `GetAllUsersForDepartmentAsync`, `CreateUserState`).

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`.
### Worker Pattern

**Entry format:**
```json
{"type": "decision|task|next|fact|blocker", "content": "one sentence max 15 words", "tags": ["topic"], "files": ["relevant/file.ts"], "date": "YYYY-MM-DD"}
Workers follow a consistent pattern (`Workers/Resgrid.Workers.Framework/Logic/`):
```csharp
public async Task<Tuple<bool, string>> Process({Type}QueueItem item)
{
try
{
// ... process item ...
return new Tuple<bool, string>(true, "");
}
catch (Exception ex)
{
Logging.LogException(ex);
return new Tuple<bool, string>(false, ex.ToString());
}
}
```

**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`.
Task type discrimination uses `(int)TaskTypes.SomeEnum`.

## Critical Gotchas & Common Bug Patterns

### 1. Billing API Response Null Safety

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

### 2. Null Plan from GetCurrentPlanForDepartmentAsync

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.

### 3. Service Locator in Constructors

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.

**Rules:**
- Only log things worth remembering across sessions (not every minor detail)
- `content` must be under 15 words
- `files` lists the files this decision/task relates to (can be empty)
- Log immediately when the item arises — not at session end
### 4. Async State Machine Line Numbers

## Session End
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.

When the user signals they are done (e.g. "bye", "done", "wrap up", "end session"), proactively update `CONTEXT.md` in the project root with:
- **Current Task**: one sentence on what was being worked on
- **Key Decisions**: bullet list, max 3 items
- **Next Steps**: bullet list, max 3 items
### 5. Cache Duration

Keep `CONTEXT.md` under 20 lines total. Do NOT summarize the full conversation — only what's needed to resume next session.
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.

## Key File Index

| Purpose | File |
|---|---|
| Solution file | `Resgrid.sln` |
| Build props | `Directory.Build.props` |
| DI Bootstrapper | `Workers/Resgrid.Workers.Framework/Bootstrapper.cs` |
| Logging | `Core/Resgrid.Framework/Logging.cs` |
| Config processor | `Core/Resgrid.Config/ConfigProcessor.cs` |
| System behavior config | `Core/Resgrid.Config/SystemBehaviorConfig.cs` |
| Cache config | `Core/Resgrid.Config/CacheConfig.cs` |
| Redis cache provider | `Providers/Resgrid.Providers.Cache/AzureRedisCacheProvider.cs` |
| Cache interface | `Core/Resgrid.Model/Providers/ICacheProvider.cs` |
| Subscriptions (billing) | `Core/Resgrid.Services/SubscriptionsService.cs` |
| Limits service | `Core/Resgrid.Services/LimitsService.cs` |
| Departments service | `Core/Resgrid.Services/DepartmentsService.cs` |
| Service interfaces | `Core/Resgrid.Model/Services/` (83 interfaces) |
| Billing API DTOs | `Core/Resgrid.Model/Billing/Api/` |
| Worker logic | `Workers/Resgrid.Workers.Framework/Logic/` |
| Worker queue items | `Core/Resgrid.Model/Queue/` |

## Common Tasks

**Build the entire solution:**
```bash
dotnet build Resgrid.sln
```

**Build a specific project:**
```bash
dotnet build Core/Resgrid.Services/Resgrid.Services.csproj
```

**Find all implementations of an interface:**
```bash
grep -r "I{Name}Service" --include="*.cs"
```
5 changes: 3 additions & 2 deletions Core/Resgrid.Config/TtsConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public static class TtsConfig
public static int DefaultSpeed = 165;
public static int MaxConcurrentGenerations = 4;
public static int MaxTextLength = 1000;
public static string EspeakExecutable = "espeak-ng";
public static string PiperExecutable = "piper";
public static string PiperModelDirectory = "/usr/local/share/piper-voices";
public static string FfmpegExecutable = "ffmpeg";
public static string TempDirectory = "";
public static string CachePrefix = "tts";
Expand Down Expand Up @@ -76,4 +77,4 @@ public static class TtsConfig
public static int RateLimitQueueLimit = 10;
public static int RateLimitWindowSeconds = 60;
}
}
}
2 changes: 1 addition & 1 deletion Core/Resgrid.Services/LimitsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ async Task<DepartmentLimits> getCurrentPlanForDepartmentAsync()
return limits;
}
}
else if ((!String.IsNullOrWhiteSpace(Config.SystemBehaviorConfig.BillingApiBaseUrl) && !String.IsNullOrWhiteSpace(Config.ApiConfig.BackendInternalApikey)) && plan.PlanId == 1)
else if (plan != null && (!String.IsNullOrWhiteSpace(Config.SystemBehaviorConfig.BillingApiBaseUrl) && !String.IsNullOrWhiteSpace(Config.ApiConfig.BackendInternalApikey)) && plan.PlanId == 1)
{
limits.PersonnelLimit = plan.GetLimitForTypeAsInt(PlanLimitTypes.Personnel);
limits.UnitsLimit = plan.GetLimitForTypeAsInt(PlanLimitTypes.Units);
Expand Down
4 changes: 2 additions & 2 deletions Core/Resgrid.Services/SubscriptionsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public SubscriptionsService(IPlansRepository plansRepository, IPaymentRepository
if (response.StatusCode == HttpStatusCode.NotFound)
return freePlan;

if (response.Data == null)
if (response.Data == null || response.Data.Data == null)
return freePlan;

return response.Data.Data;
Expand Down Expand Up @@ -109,7 +109,7 @@ public async Task<DepartmentPlanCount> GetPlanCountsForDepartmentAsync(int depar
if (response.StatusCode == HttpStatusCode.NotFound)
return new DepartmentPlanCount();

if (response.Data == null)
if (response.Data == null || response.Data.Data == null)
return new DepartmentPlanCount();

return response.Data.Data;
Expand Down
Loading
Loading