Skip to content

Commit 1672464

Browse files
authored
Merge pull request #362 from Resgrid/develop
RE1-T115 Changing TTS voice provider.
2 parents 6ebc6b8 + 98109a4 commit 1672464

21 files changed

Lines changed: 1438 additions & 828 deletions

CLAUDE.md

Lines changed: 212 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,244 @@
1-
<!-- dgc-policy-v11 -->
2-
# Dual-Graph Context Policy
1+
# Resgrid Project Guide
32

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

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

8-
**Call `graph_continue` ONLY when you do NOT already know the relevant files.**
7+
## Solution Structure
98

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`
1460

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/`
1964

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

22-
## When you DO call graph_continue
67+
### Layered Architecture
2368

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+
```
2572

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
2780

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)
3182

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:
3684

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+
```
38122

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

41-
```json
125+
**Common pattern in Services** (local function + cache wrapper):
126+
```csharp
127+
public async Task<Foo> GetFooAsync(int departmentId, bool bypassCache = false)
42128
{
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();
48139
}
49140
```
50141

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

53-
## Token Usage
144+
### Logging
54145

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+
```
56152

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]`.
60154

61-
## Rules
155+
### Naming Conventions
62156

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}/` |
69162

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

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
73166

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+
}
77182
```
78183

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

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
86201

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

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
93205

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+
```

Core/Resgrid.Config/TtsConfig.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public static class TtsConfig
2828
public static int DefaultSpeed = 165;
2929
public static int MaxConcurrentGenerations = 4;
3030
public static int MaxTextLength = 1000;
31-
public static string EspeakExecutable = "espeak-ng";
31+
public static string PiperExecutable = "piper";
32+
public static string PiperModelDirectory = "/usr/local/share/piper-voices";
3233
public static string FfmpegExecutable = "ffmpeg";
3334
public static string TempDirectory = "";
3435
public static string CachePrefix = "tts";
@@ -76,4 +77,4 @@ public static class TtsConfig
7677
public static int RateLimitQueueLimit = 10;
7778
public static int RateLimitWindowSeconds = 60;
7879
}
79-
}
80+
}

Core/Resgrid.Services/LimitsService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ async Task<DepartmentLimits> getCurrentPlanForDepartmentAsync()
206206
return limits;
207207
}
208208
}
209-
else if ((!String.IsNullOrWhiteSpace(Config.SystemBehaviorConfig.BillingApiBaseUrl) && !String.IsNullOrWhiteSpace(Config.ApiConfig.BackendInternalApikey)) && plan.PlanId == 1)
209+
else if (plan != null && (!String.IsNullOrWhiteSpace(Config.SystemBehaviorConfig.BillingApiBaseUrl) && !String.IsNullOrWhiteSpace(Config.ApiConfig.BackendInternalApikey)) && plan.PlanId == 1)
210210
{
211211
limits.PersonnelLimit = plan.GetLimitForTypeAsInt(PlanLimitTypes.Personnel);
212212
limits.UnitsLimit = plan.GetLimitForTypeAsInt(PlanLimitTypes.Units);

Core/Resgrid.Services/SubscriptionsService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public SubscriptionsService(IPlansRepository plansRepository, IPaymentRepository
7474
if (response.StatusCode == HttpStatusCode.NotFound)
7575
return freePlan;
7676

77-
if (response.Data == null)
77+
if (response.Data == null || response.Data.Data == null)
7878
return freePlan;
7979

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

112-
if (response.Data == null)
112+
if (response.Data == null || response.Data.Data == null)
113113
return new DepartmentPlanCount();
114114

115115
return response.Data.Data;

0 commit comments

Comments
 (0)