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
189 changes: 189 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<!-- dgc-policy-v11 -->
# Dual-Graph Context Policy

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

## MANDATORY: Adaptive graph_continue rule

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

### 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

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

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

## When you DO call graph_continue

1. **If `graph_continue` returns `needs_project=true`**: call `graph_scan` with `pwd`. Do NOT ask the user.

2. **If `graph_continue` returns `skip=true`**: fewer than 5 files — read only specifically named files.

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.

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.

## Session State (compact, update after every turn)

Maintain a short JSON block in your working memory. Update it after each turn:

```json
{
"files_identified": ["path/to/file.py"],
"symbols_changed": ["module::function"],
"fix_applied": true,
"features_added": ["description"],
"open_issues": ["one-line note"]
}
```

Use this state — not prose summaries — to remember what's been done across turns.

## Token Usage

A `token-counter` MCP is available for tracking live token usage.

- 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"})`

## Rules

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

## Context Store

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

**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"}
```

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

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

## Session End

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

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

Use one English variant consistently in this document.

Line 94 uses “summarize” while other sections use UK spelling (“minimise”, “organisation”). Please normalize to one dialect for consistency.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~94-~94: Do not mix variants of the same word (‘summarize’ and ‘summarise’) within a single text.
Context: ...ONTEXT.md` under 20 lines total. Do NOT summarize the full conversation — only what's nee...

(EN_WORD_COHERENCY)

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

In `@CLAUDE.md` at line 94, The document uses mixed English variants; normalize to
UK English to match existing words like “minimise” and “organisation” by
replacing “summarize” on line 94 with the UK spelling “summarise” and scan
nearby text for any other US spellings to convert (e.g., "organize" ->
"organise") so the CLAUDE.md uses one consistent dialect.


---

# Coding Standards

## General

Write C# code that maximises readability, maintainability, and correctness while minimising complexity and coupling. Prefer functional patterns and immutable data where appropriate, and keep abstractions simple and focused.

- Write clear, self-documenting code
- Keep abstractions simple and focused
- Minimise dependencies and coupling
- Use modern C# features appropriately
- Use the repository pattern with Dapper at the repository layer for SQL Server and PostgreSQL database communication

## Code Organisation

**Use meaningful names — no unclear abbreviations:**
```csharp
// Good
public async Task<Result<Order>> ProcessOrderAsync(OrderRequest request, CancellationToken cancellationToken)

// Avoid
public async Task<Result<T>> ProcAsync<T>(ReqDto r, CancellationToken ct)
```

**Separate state from behaviour:**
```csharp
// Good
public sealed record Order(OrderId Id, List<OrderLine> Lines);

public static class OrderOperations
{
public static decimal CalculateTotal(Order order) =>
order.Lines.Sum(line => line.Price * line.Quantity);
}
```

**Prefer pure methods — avoid hidden side effects:**
```csharp
// Good
public static decimal CalculateTotalPrice(IEnumerable<OrderLine> lines, decimal taxRate) =>
lines.Sum(line => line.Price * line.Quantity) * (1 + taxRate);

// Avoid
public void CalculateAndUpdateTotalPrice()
{
this.Total = this.Lines.Sum(l => l.Price * l.Quantity);
this.UpdateDatabase();
}
```

**Use extension methods for domain-specific operations:**
```csharp
public static class OrderExtensions
{
public static bool CanBeFulfilled(this Order order, Inventory inventory) =>
order.Lines.All(line => inventory.HasStock(line.ProductId, line.Quantity));
}
```

**Design for testability — avoid hidden dependencies:**
```csharp
// Good: pure, easily testable
public static decimal CalculateDiscount(decimal price, int quantity, CustomerTier tier) => ...

// Avoid: hidden service calls make this impossible to unit test
public decimal CalculateDiscount()
{
var user = _userService.GetCurrentUser();
var settings = _configService.GetSettings();
...
}
```

## Dependency Management

**Minimise constructor injection — too many dependencies signal a design problem:**
```csharp
// Good
public sealed class OrderProcessor(IOrderRepository repository) { }

// Avoid
public class OrderProcessor(
IOrderRepository repository,
ILogger logger,
IEmailService emailService,
IMetrics metrics,
IValidator validator) { }
```

**Prefer composition via interfaces:**
```csharp
public sealed class EnhancedLogger(ILogger baseLogger, IMetrics metrics) : ILogger { }
```
59 changes: 54 additions & 5 deletions Core/Resgrid.Localization/Areas/User/Routes/Routes.en.resx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@
<value>New Route</value>
</data>
<data name="ActiveRoutes" xml:space="preserve">
<value>Active Routes</value>
<value>In Progress</value>
</data>
<data name="ArchivedRoutes" xml:space="preserve">
<value>Archived Routes</value>
</data>
<data name="Stops" xml:space="preserve">
<value>Stops</value>
Expand Down Expand Up @@ -135,20 +138,38 @@
</data>
<!-- ActiveRoutes -->
<data name="ActiveRoutesPageTitle" xml:space="preserve">
<value>Active Routes</value>
<value>In Progress Routes</value>
</data>
<data name="ActiveRoutesHeader" xml:space="preserve">
<value>Active Routes</value>
<value>In Progress Routes</value>
</data>
<data name="ActiveBreadcrumb" xml:space="preserve">
<value>Active</value>
<value>In Progress</value>
</data>
<data name="NoActiveRoutes" xml:space="preserve">
<value>No Active Routes</value>
<value>No Routes In Progress</value>
</data>
<data name="NoActiveRoutesMessage" xml:space="preserve">
<value>There are no route instances currently in progress.</value>
</data>
<data name="ArchivedRoutesPageTitle" xml:space="preserve">
<value>Archived Routes</value>
</data>
<data name="ArchivedRoutesHeader" xml:space="preserve">
<value>Archived Routes</value>
</data>
<data name="ArchivedBreadcrumb" xml:space="preserve">
<value>Archived</value>
</data>
<data name="NoArchivedRoutes" xml:space="preserve">
<value>No Archived Routes</value>
</data>
<data name="NoArchivedRoutesMessage" xml:space="preserve">
<value>There are no archived route plans.</value>
</data>
<data name="ArchivedRouteDetailPageTitle" xml:space="preserve">
<value>Archived Route Detail</value>
</data>
<data name="UnitLabel" xml:space="preserve">
<value>Unit</value>
</data>
Expand Down Expand Up @@ -231,6 +252,34 @@
<data name="CreateRoute" xml:space="preserve">
<value>Create Route</value>
</data>
<!-- StartRoute -->
<data name="StartRoutePageTitle" xml:space="preserve">
<value>Start Route</value>
</data>
<data name="StartRouteHeader" xml:space="preserve">
<value>Start Route</value>
</data>
<data name="StartRouteBreadcrumb" xml:space="preserve">
<value>Start Route</value>
</data>
<data name="AssignUnit" xml:space="preserve">
<value>Assign Unit</value>
</data>
<data name="SelectUnitPlaceholder" xml:space="preserve">
<value>-- Select a Unit --</value>
</data>
<data name="AssignUnitHelp" xml:space="preserve">
<value>Select the unit that will execute this route.</value>
</data>
<data name="RouteInfoLabel" xml:space="preserve">
<value>Route Info</value>
</data>
<data name="MinEstimated" xml:space="preserve">
<value>min estimated</value>
</data>
<data name="StartRouteNow" xml:space="preserve">
<value>Start Route Now</value>
</data>
<!-- Edit -->
<data name="EditRoutePageTitle" xml:space="preserve">
<value>Edit Route</value>
Expand Down
5 changes: 5 additions & 0 deletions Core/Resgrid.Model/Services/IRouteService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,10 @@ public interface IRouteService

// Geofence
Task<RouteInstanceStop> CheckGeofenceProximityAsync(int unitId, decimal latitude, decimal longitude);

// Single-record lookups
Task<RouteStop> GetRouteStopByIdAsync(string routeStopId);
Task<RouteInstanceStop> GetInstanceStopByIdAsync(string routeInstanceStopId);
Task<RouteInstanceStop> UpdateInstanceStopNotesAsync(string routeInstanceStopId, string notes, CancellationToken cancellationToken = default);
}
}
25 changes: 25 additions & 0 deletions Core/Resgrid.Services/RouteService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,31 @@ public async Task<RoutePlan> UpdateRouteGeometryAsync(string routePlanId, string

#endregion Mapbox Integration

#region Single-record lookups

public async Task<RouteStop> GetRouteStopByIdAsync(string routeStopId)
{
return await _routeStopsRepository.GetByIdAsync(routeStopId);
}

public async Task<RouteInstanceStop> GetInstanceStopByIdAsync(string routeInstanceStopId)
{
return await _routeInstanceStopsRepository.GetByIdAsync(routeInstanceStopId);
}

public async Task<RouteInstanceStop> UpdateInstanceStopNotesAsync(string routeInstanceStopId, string notes, CancellationToken cancellationToken = default)
{
var instanceStop = await _routeInstanceStopsRepository.GetByIdAsync(routeInstanceStopId);
if (instanceStop == null)
throw new ArgumentException("Route instance stop not found.", nameof(routeInstanceStopId));

instanceStop.Notes = notes;
await _routeInstanceStopsRepository.SaveOrUpdateAsync(instanceStop, cancellationToken);
return instanceStop;
}

#endregion Single-record lookups

#region Geofence

public async Task<RouteInstanceStop> CheckGeofenceProximityAsync(int unitId, decimal latitude, decimal longitude)
Expand Down
Loading
Loading