Companion to the design spec, kept for reference alongside the executed work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
56 KiB
Remove Inventory Quantity Tracking Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Remove StockItem.QuantityOnHand and the entire StockTransaction ledger from CutList so a job's stock is always exactly what the user explicitly enters (with -1 meaning unlimited, everywhere that option exists) — no disconnected inventory-quantity system for users to think about.
Architecture: This is a subtractive refactor across every layer that touches StockItem/StockTransaction: entities, EF Core config + migration, services, controllers/DTOs, the packing engine's now-removed auto-discovery fallback, the Blazor UI, and the MCP server. Work proceeds "leaves before root" — UI and API surface are trimmed first while the underlying field/entity still exist (so every intermediate task still compiles), and the actual StockItem.QuantityOnHand column drop / StockTransaction table drop happens last, once nothing references it.
Tech Stack: .NET 8 Blazor Server (CutList.Web), .NET 8 EF Core + SQL Server, .NET 10 MCP stdio server (CutList.Mcp).
Global Constraints
- No automated test suite exists in this repo today (confirmed during spec review). Verification for every task is: the solution builds clean,
get_diagnosticson the touched project(s) shows no new errors, and — for behavior-affecting tasks — a manual pass through the running app (per the project's own guidance: "For UI or frontend changes, start the dev server and use the feature in a browser before reporting the task as complete"). - Per user's global CLAUDE.md: use Roslyn Bridge MCP tools (
mcp__roslyn-bridge__*) as the primary method for C# code exploration/diagnostics rather than Glob/Grep where a Roslyn Bridge tool fits. - Per project CLAUDE.md: after creating an EF Core migration, apply it immediately with
dotnet ef database update --project CutList.Web— do not leave it pending. - Per project CLAUDE.md: CutList.Mcp is republished after any change, to
~/.claude/mcp/CutList.Mcp/, viadotnet publish CutList.Mcp/CutList.Mcp.csproj -c Release -o "$USERPROFILE/.claude/mcp/CutList.Mcp". - Commit after each task with a message describing why, not just what (repo convention — see recent commits like
b77d0d9,f75c205).
Task 1: Strip QuantityOnHand display from the Blazor UI
Files:
- Modify:
CutList.Web/Components/Pages/Stock/Index.razor - Modify:
CutList.Web/Components/Pages/Stock/Edit.razor - Modify:
CutList.Web/Components/Pages/Jobs/Edit.razor
Interfaces:
- Consumes:
StockItem.QuantityOnHand(still present on the entity at this point — removed in Task 6).StockItemService.AddStockAsync/AdjustStockAsync/ScrapStockAsync/GetTransactionHistoryAsync(still present — removed in Task 2). - Produces: nothing new; this task only deletes UI surface. No other task depends on what this one produces.
This task only removes display and interaction of quantity/transactions from the UI. The underlying service methods and entity field are removed in later tasks — leaving them in place for now means the app still builds and runs after this task, even though the "Add/Adjust Stock" UI is now gone.
- Step 1: Remove the "On Hand" column from
/stock(Index)
In CutList.Web/Components/Pages/Stock/Index.razor, remove the <th>On Hand</th> header and its matching <td> cell:
<th>Length</th>
<th>On Hand</th>
<th style="width: 100px;">Actions</th>
becomes:
<th>Length</th>
<th style="width: 100px;">Actions</th>
and:
<td>@ArchUnits.FormatFromInches((double)item.LengthInches)</td>
<td>
@if (item.QuantityOnHand > 0)
{
<span class="badge bg-success">@item.QuantityOnHand</span>
}
else
{
<span class="badge bg-secondary">0</span>
}
</td>
<td>
becomes:
<td>@ArchUnits.FormatFromInches((double)item.LengthInches)</td>
<td>
Also update the intro paragraph — it currently says stock items track "how many pieces you have on hand":
<p class="text-muted mb-4">
Stock items represent the specific lengths of material you have available for cutting. Each stock item links
a material to a length and tracks how many pieces you have on hand.
</p>
becomes:
<p class="text-muted mb-4">
Stock items represent the specific lengths of material you can cut from. Add the lengths you typically
work with here, then pick from them when adding stock to a job.
</p>
- Step 2: Replace
Stock/Edit.razorwith the quantity/transaction UI removed
Replace the full contents of CutList.Web/Components/Pages/Stock/Edit.razor with:
@page "/stock/new"
@page "/stock/{Id:int}"
@inject StockItemService StockItemService
@inject MaterialService MaterialService
@inject NavigationManager Navigation
@using CutList.Core.Formatting
<PageTitle>@(IsNew ? "Add Stock Item" : "Edit Stock Item")</PageTitle>
<h1>@(IsNew ? "Add Stock Item" : $"{stockItem.Material?.DisplayName} - {ArchUnits.FormatFromInches((double)stockItem.LengthInches)}")</h1>
@if (loading)
{
<p><em>Loading...</em></p>
}
else
{
<div class="row">
<div class="col-lg-6 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Stock Item Details</h5>
</div>
<div class="card-body">
<EditForm Model="stockItem" OnValidSubmit="SaveStockItemAsync">
<DataAnnotationsValidator />
<div class="mb-3">
<label class="form-label">Material</label>
<select class="form-select" @bind="stockItem.MaterialId" disabled="@(!IsNew)">
<option value="0">-- Select Material --</option>
@foreach (var material in materials)
{
<option value="@material.Id">@material.DisplayName</option>
}
</select>
</div>
<div class="mb-3">
<label class="form-label">Length</label>
<LengthInput @bind-Value="stockItem.LengthInches" />
</div>
<div class="mb-3">
<label class="form-label">Name (optional)</label>
<InputText class="form-control" @bind-Value="stockItem.Name" placeholder="Custom display name" />
</div>
<div class="mb-3">
<label class="form-label">Notes (optional)</label>
<InputText class="form-control" @bind-Value="stockItem.Notes" placeholder="Internal notes" />
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger">@errorMessage</div>
}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" disabled="@saving">
@if (saving)
{
<span class="spinner-border spinner-border-sm me-1"></span>
}
@(IsNew ? "Create Stock Item" : "Save Changes")
</button>
<a href="stock" class="btn btn-outline-secondary">@(IsNew ? "Cancel" : "Back to List")</a>
</div>
</EditForm>
</div>
</div>
</div>
</div>
}
@code {
[Parameter]
public int? Id { get; set; }
private StockItem stockItem = new();
private List<Material> materials = new();
private bool loading = true;
private bool saving;
private string? errorMessage;
private bool IsNew => !Id.HasValue;
protected override async Task OnInitializedAsync()
{
materials = await MaterialService.GetAllAsync();
if (Id.HasValue)
{
var existing = await StockItemService.GetByIdAsync(Id.Value);
if (existing == null)
{
Navigation.NavigateTo("stock");
return;
}
stockItem = existing;
}
loading = false;
}
private async Task SaveStockItemAsync()
{
errorMessage = null;
saving = true;
try
{
if (stockItem.MaterialId == 0)
{
errorMessage = "Please select a material";
return;
}
if (stockItem.LengthInches <= 0)
{
errorMessage = "Length must be greater than zero";
return;
}
var exists = await StockItemService.ExistsAsync(
stockItem.MaterialId,
stockItem.LengthInches,
IsNew ? null : stockItem.Id);
if (exists)
{
errorMessage = "A stock item with this material and length already exists";
return;
}
try
{
if (IsNew)
{
var created = await StockItemService.CreateAsync(stockItem);
Navigation.NavigateTo($"stock/{created.Id}");
}
else
{
await StockItemService.UpdateAsync(stockItem);
}
}
catch (Microsoft.EntityFrameworkCore.DbUpdateException)
{
errorMessage = "A stock item with this material and length already exists (it may have been previously deleted).";
}
}
finally
{
saving = false;
}
}
}
Notes on what changed: dropped the @using CutList.Web.Data.Entities (no longer needed — StockTransaction/StockTransactionType aren't referenced), dropped the entire right-hand "Inventory" card and its @code support (transactions, showStockForm, savingStockTransaction, stockTransactionType, stockQuantity, stockNotes, stockFormErrorMessage, GetTransactionBadgeClass, ShowStockForm, CancelStockForm, SaveStockTransactionAsync), and dropped the transactions = await StockItemService.GetTransactionHistoryAsync(...) call from OnInitializedAsync.
- Step 3: Remove QuantityOnHand references from
Jobs/Edit.razor
In the "Import from Inventory" candidates table, remove the "On Hand" column:
<thead>
<tr>
<th style="width: 40px;"></th>
<th>Length</th>
<th>On Hand</th>
<th style="width: 120px;">Qty to Use</th>
<th style="width: 100px;">Priority</th>
</tr>
</thead>
becomes:
<thead>
<tr>
<th style="width: 40px;"></th>
<th>Length</th>
<th style="width: 120px;">Qty to Use</th>
<th style="width: 100px;">Priority</th>
</tr>
</thead>
and remove the matching data cell:
<td>@ArchUnits.FormatFromInches((double)candidate.StockItem.LengthInches)</td>
<td>@candidate.StockItem.QuantityOnHand</td>
<td>
becomes:
<td>@ArchUnits.FormatFromInches((double)candidate.StockItem.LengthInches)</td>
<td>
In the "Add Stock from Inventory" length dropdown, drop the "(N available)" suffix:
<option value="@stock.Id">@ArchUnits.FormatFromInches((double)stock.LengthInches) (@stock.QuantityOnHand available)</option>
becomes:
<option value="@stock.Id">@ArchUnits.FormatFromInches((double)stock.LengthInches)</option>
- Step 4: Build and check diagnostics
Run:
mcp__RoslynBridge__get_diagnostics_summary # for CutList.Web
dotnet build CutList.Web/CutList.Web.csproj
Expected: build succeeds with 0 errors (unused-using warnings for CutList.Web.Data.Entities in Stock/Edit.razor, if any, are fine — this repo doesn't treat warnings as errors).
- Step 5: Manual check
Start the app (dotnet run --project CutList.Web/CutList.Web.csproj), open /stock, confirm the list renders without an On Hand column and the intro copy reads correctly; open an existing stock item's edit page and confirm there's no Inventory card; open a job's Stock tab and confirm the "Add Stock from Inventory" dropdown and "Import from Inventory" modal both render without quantity figures.
- Step 6: Commit
git add CutList.Web/Components/Pages/Stock/Index.razor CutList.Web/Components/Pages/Stock/Edit.razor CutList.Web/Components/Pages/Jobs/Edit.razor
git commit -m "$(cat <<'EOF'
refactor: remove QuantityOnHand display from stock and job UI
First step of removing inventory quantity tracking (see
docs/superpowers/specs/2026-08-01-remove-inventory-quantity-tracking-design.md).
UI no longer shows on-hand counts or offers transaction entry; the
underlying field/service methods are removed in follow-up commits.
EOF
)"
Task 2: Remove stock-transaction service methods, controller endpoints, and DTOs
Files:
- Modify:
CutList.Web/Services/StockItemService.cs - Modify:
CutList.Web/Controllers/StockItemsController.cs - Modify:
CutList.Web/Controllers/JobsController.cs:297-316 - Modify:
CutList.Web/DTOs/StockItemDtos.cs
Interfaces:
-
Consumes: nothing from Task 1.
-
Produces:
StockItemDtowith noQuantityOnHandproperty,CreateStockItemDtowith noQuantityOnHandproperty — Task 5 (MCP client) must match this shape. -
Step 1: Remove transaction methods from
StockItemService
In CutList.Web/Services/StockItemService.cs, delete the entire // Stock transaction methods region — everything from public async Task<StockTransaction> AddStockAsync(...) through the end of RecalculateQuantityAsync, i.e. delete lines 112-253 (from the // Stock transaction methods comment through the closing brace of RecalculateQuantityAsync), leaving the file ending after ExistsAsync:
public async Task<bool> ExistsAsync(int materialId, decimal lengthInches, int? excludeId = null)
{
await using var context = _factory.CreateDbContext();
var query = context.StockItems.Where(s =>
s.MaterialId == materialId &&
s.LengthInches == lengthInches &&
s.IsActive);
if (excludeId.HasValue)
{
query = query.Where(s => s.Id != excludeId.Value);
}
return await query.AnyAsync();
}
}
- Step 2: Remove transaction endpoints from
StockItemsController
In CutList.Web/Controllers/StockItemsController.cs, delete these six action methods entirely: GetTransactions (GET {id}/transactions), ReceiveStock (POST {id}/receive), UseStock (POST {id}/use), AdjustStock (POST {id}/adjust), ScrapStock (POST {id}/scrap), RecalculateStock (POST {id}/recalculate) — i.e. everything between the end of GetByMaterial and the start of MapToDto.
Update Create, which currently sets QuantityOnHand on the new entity:
var stockItem = new StockItem
{
MaterialId = dto.MaterialId,
LengthInches = (decimal)lengthInches,
Name = dto.Name,
QuantityOnHand = dto.QuantityOnHand,
Notes = dto.Notes
};
becomes:
var stockItem = new StockItem
{
MaterialId = dto.MaterialId,
LengthInches = (decimal)lengthInches,
Name = dto.Name,
Notes = dto.Notes
};
Update MapToDto:
private static StockItemDto MapToDto(StockItem s) => new()
{
Id = s.Id,
MaterialId = s.MaterialId,
MaterialName = s.Material?.DisplayName ?? string.Empty,
LengthInches = s.LengthInches,
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
Name = s.Name,
QuantityOnHand = s.QuantityOnHand,
Notes = s.Notes,
IsActive = s.IsActive
};
private static StockTransactionDto MapTransactionToDto(StockTransaction t) => new()
{
Id = t.Id,
StockItemId = t.StockItemId,
Quantity = t.Quantity,
Type = t.Type.ToString(),
JobId = t.JobId,
JobNumber = t.Job?.JobNumber,
Notes = t.Notes,
CreatedAt = t.CreatedAt
};
}
becomes:
private static StockItemDto MapToDto(StockItem s) => new()
{
Id = s.Id,
MaterialId = s.MaterialId,
MaterialName = s.Material?.DisplayName ?? string.Empty,
LengthInches = s.LengthInches,
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
Name = s.Name,
Notes = s.Notes,
IsActive = s.IsActive
};
}
- Step 3: Fix
JobsController.GetAvailableStock, which also mapsQuantityOnHand
In CutList.Web/Controllers/JobsController.cs:
var items = await _jobService.GetAvailableStockForMaterialAsync(materialId);
return Ok(items.Select(s => new StockItemDto
{
Id = s.Id,
MaterialId = s.MaterialId,
MaterialName = s.Material?.DisplayName ?? string.Empty,
LengthInches = s.LengthInches,
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
Name = s.Name,
QuantityOnHand = s.QuantityOnHand,
IsActive = s.IsActive
}).ToList());
becomes:
var items = await _jobService.GetAvailableStockForMaterialAsync(materialId);
return Ok(items.Select(s => new StockItemDto
{
Id = s.Id,
MaterialId = s.MaterialId,
MaterialName = s.Material?.DisplayName ?? string.Empty,
LengthInches = s.LengthInches,
LengthFormatted = ArchUnits.FormatFromInches((double)s.LengthInches),
Name = s.Name,
IsActive = s.IsActive
}).ToList());
- Step 4: Trim
StockItemDtos.cs
Replace the full contents of CutList.Web/DTOs/StockItemDtos.cs with:
namespace CutList.Web.DTOs;
public class StockItemDto
{
public int Id { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public decimal LengthInches { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
public string? Name { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
}
public class CreateStockItemDto
{
public int MaterialId { get; set; }
public string Length { get; set; } = string.Empty;
public string? Name { get; set; }
public string? Notes { get; set; }
}
public class UpdateStockItemDto
{
public string? Length { get; set; }
public string? Name { get; set; }
public string? Notes { get; set; }
}
This deletes StockTransactionDto, AddStockDto, UseStockDto, AdjustStockDto, ScrapStockDto.
- Step 5: Build and check diagnostics
Run:
mcp__RoslynBridge__get_diagnostics_summary # for CutList.Web
dotnet build CutList.Web/CutList.Web.csproj
Expected: 0 errors. (CutListPackingService and CatalogService still reference StockItem.QuantityOnHand directly on the entity, which still exists — untouched until Tasks 3/4/6.)
- Step 6: Manual check
With the app running, confirm /stock/new still creates a stock item successfully, and Swagger (/swagger in Development) no longer lists the removed stock-items sub-routes.
- Step 7: Commit
git add CutList.Web/Services/StockItemService.cs CutList.Web/Controllers/StockItemsController.cs CutList.Web/Controllers/JobsController.cs CutList.Web/DTOs/StockItemDtos.cs
git commit -m "$(cat <<'EOF'
refactor: remove stock-transaction service methods, endpoints, and DTOs
No job workflow ever called these (receive/use/adjust/scrap/recalculate)
- inventory quantity tracking is being removed per
docs/superpowers/specs/2026-08-01-remove-inventory-quantity-tracking-design.md.
EOF
)"
Task 3: Remove the packing engine's auto-discovery fallback
Files:
- Modify:
CutList.Web/Services/CutListPackingService.cs:64-94
Interfaces:
-
Consumes:
JobStock(unchanged),StockItem.QuantityOnHand(still present on entity, read here for the last time before Task 6 removes it). -
Produces:
PackAsyncnow requires job-specificJobStockrows to produce any stock bins for a material — later tasks/manual verification must account for this. -
Step 1: Delete the fallback branch
In CutList.Web/Services/CutListPackingService.cs, the if/else that builds stockBins currently reads:
// Check if job has specific stock configured for this material
if (jobStockByMaterial.TryGetValue(materialId, out var materialJobStock) && materialJobStock.Count > 0)
{
// Use job-specific stock configuration
foreach (var stock in materialJobStock.OrderBy(s => s.Priority))
{
stockBins.Add(new StockBinSource
{
LengthInches = stock.LengthInches,
Quantity = stock.Quantity,
Priority = stock.Priority,
IsInStock = !stock.IsCustomLength && stock.StockItemId.HasValue
});
}
}
else
{
// No job-specific stock - use all available stock items for this material
var stockItems = await context.StockItems
.Where(s => s.MaterialId == materialId && s.IsActive)
.ToListAsync();
foreach (var stock in stockItems)
{
if (stock.QuantityOnHand > 0)
{
// In-stock with finite quantity
stockBins.Add(new StockBinSource
{
LengthInches = stock.LengthInches,
Quantity = stock.QuantityOnHand,
Priority = 1,
IsInStock = true
});
}
// Always add as purchasable (unlimited) - algorithm will use in-stock first due to priority
stockBins.Add(new StockBinSource
{
LengthInches = stock.LengthInches,
Quantity = -1, // unlimited
Priority = 2,
IsInStock = false
});
}
}
becomes:
// Use job-specific stock configuration. Jobs must have stock explicitly configured -
// there is no fallback to "whatever's in inventory."
if (jobStockByMaterial.TryGetValue(materialId, out var materialJobStock))
{
foreach (var stock in materialJobStock.OrderBy(s => s.Priority))
{
stockBins.Add(new StockBinSource
{
LengthInches = stock.LengthInches,
Quantity = stock.Quantity,
Priority = stock.Priority,
IsInStock = !stock.IsCustomLength && stock.StockItemId.HasValue
});
}
}
Note this also drops the now-unreachable .Count > 0 check (an empty List<JobStock> for a material key can't occur — jobStockByMaterial is built from a non-empty grouping — but keeping the plain TryGetValue matches the simplified intent and avoids a dead condition).
The context field on PackAsync (await using var context = _factory.CreateDbContext();) is still used elsewhere in the method (await context.Materials.FirstOrDefaultAsync(...)), so no other changes are needed in this file.
- Step 2: Build and check diagnostics
Run:
mcp__RoslynBridge__get_diagnostics_summary # for CutList.Web
dotnet build CutList.Web/CutList.Web.csproj
Expected: 0 errors.
- Step 3: Manual check
With the app running: create (or reuse) a job with parts for a material, and do not add any Stock rows for that material, then run Optimize. Confirm all of that material's parts now show up under "Items Not Placed" instead of being silently packed from inventory. Then add explicit Stock (both a finite-quantity catalog item and an unlimited custom length) and re-optimize, confirming packing now works and respects the finite quantity as a hard ceiling.
- Step 4: Commit
git add CutList.Web/Services/CutListPackingService.cs
git commit -m "$(cat <<'EOF'
refactor: remove packing auto-discovery fallback
The fallback silently pulled from StockItem.QuantityOnHand and always
added an extra unlimited bin on top when a job had no stock configured
- i.e. it assumed unlimited purchasing. Per
docs/superpowers/specs/2026-08-01-remove-inventory-quantity-tracking-design.md,
a job's available stock must now be exactly what's explicitly entered.
EOF
)"
Task 4: Remove QuantityOnHand from catalog import/export
Files:
- Modify:
CutList.Web/DTOs/CatalogDtos.cs:102-108 - Modify:
CutList.Web/Services/CatalogService.cs:335-394
Interfaces:
-
Consumes: nothing from prior tasks.
-
Produces:
CatalogStockItemDtowith noQuantityOnHand— existing seed JSON (alro-catalog.json,oneals-catalog.json) keeps the field in the file;System.Text.Jsonignores unknown properties on deserialize by default, so no seed-file edits are required. -
Step 1: Drop the field from
CatalogStockItemDto
In CutList.Web/DTOs/CatalogDtos.cs:
public class CatalogStockItemDto
{
public decimal LengthInches { get; set; }
public string? Name { get; set; }
public int QuantityOnHand { get; set; }
public string? Notes { get; set; }
}
becomes:
public class CatalogStockItemDto
{
public decimal LengthInches { get; set; }
public string? Name { get; set; }
public string? Notes { get; set; }
}
- Step 2: Stop reading/writing it in
CatalogService
In CutList.Web/Services/CatalogService.cs, in ImportStockItemsAsync:
var stockItem = new StockItem
{
MaterialId = material.Id,
LengthInches = dto.LengthInches,
Name = dto.Name,
QuantityOnHand = dto.QuantityOnHand,
Notes = dto.Notes,
CreatedAt = DateTime.UtcNow
};
becomes:
var stockItem = new StockItem
{
MaterialId = material.Id,
LengthInches = dto.LengthInches,
Name = dto.Name,
Notes = dto.Notes,
CreatedAt = DateTime.UtcNow
};
In MapStockItems:
private static List<CatalogStockItemDto> MapStockItems(Material m)
{
return m.StockItems.OrderBy(s => s.LengthInches).Select(s => new CatalogStockItemDto
{
LengthInches = s.LengthInches,
Name = s.Name,
QuantityOnHand = s.QuantityOnHand,
Notes = s.Notes
}).ToList();
}
becomes:
private static List<CatalogStockItemDto> MapStockItems(Material m)
{
return m.StockItems.OrderBy(s => s.LengthInches).Select(s => new CatalogStockItemDto
{
LengthInches = s.LengthInches,
Name = s.Name,
Notes = s.Notes
}).ToList();
}
- Step 3: Build and check diagnostics
Run:
mcp__RoslynBridge__get_diagnostics_summary # for CutList.Web
dotnet build CutList.Web/CutList.Web.csproj
Expected: 0 errors.
- Step 4: Manual check
If a Catalog import/export flow is easy to exercise locally (via CatalogController/Swagger or scripts/ExportData), run an export and confirm the resulting JSON's stock items no longer include quantityOnHand. If not convenient, the build passing plus code review of the diff is sufficient — this is a pure data-shape change with no algorithmic effect.
- Step 5: Commit
git add CutList.Web/DTOs/CatalogDtos.cs CutList.Web/Services/CatalogService.cs
git commit -m "$(cat <<'EOF'
refactor: drop QuantityOnHand from catalog import/export
Part of removing inventory quantity tracking; existing seed JSON keeps
the field on disk but it's now ignored on import.
EOF
)"
Task 5: Update CutList.Mcp and republish
Files:
- Modify:
CutList.Mcp/ApiClient.cs:73-91,355-366 - Modify:
CutList.Mcp/InventoryTools.cs:139-357,514-552
Interfaces:
-
Consumes:
StockItemDto/CreateStockItemDtoshape from Task 2 (noQuantityOnHand) — the REST API this project calls has already dropped the field, so this task brings the MCP client in sync. -
Produces:
add_stock_itemandadd_stockMCP tools with noquantityOnHand/QuantityOnHandparameters or fields. -
Step 1: Update
ApiClient.cs
ApiStockItemDto currently:
public class ApiStockItemDto
{
public int Id { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public decimal LengthInches { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
public string? Name { get; set; }
public int QuantityOnHand { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
}
becomes:
public class ApiStockItemDto
{
public int Id { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public decimal LengthInches { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
public string? Name { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
}
CreateStockItemAsync currently:
public async Task<ApiStockItemDto?> CreateStockItemAsync(int materialId, string length, string? name, int quantityOnHand, string? notes)
{
var body = new
{
MaterialId = materialId,
Length = length,
Name = name,
QuantityOnHand = quantityOnHand,
Notes = notes
};
becomes:
public async Task<ApiStockItemDto?> CreateStockItemAsync(int materialId, string length, string? name, string? notes)
{
var body = new
{
MaterialId = materialId,
Length = length,
Name = name,
Notes = notes
};
- Step 2: Update
InventoryTools.cs—StockItemDtoandListStockItems
The MCP-local StockItemDto:
public class StockItemDto
{
public int Id { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public decimal LengthInches { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
public string? Name { get; set; }
public int QuantityOnHand { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
}
becomes:
public class StockItemDto
{
public int Id { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public decimal LengthInches { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
public string? Name { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
}
In ListStockItems, the mapping:
StockItems = items.Select(s => new StockItemDto
{
Id = s.Id,
MaterialId = s.MaterialId,
MaterialName = s.MaterialName,
LengthInches = s.LengthInches,
LengthFormatted = s.LengthFormatted,
Name = s.Name,
QuantityOnHand = s.QuantityOnHand,
Notes = s.Notes,
IsActive = s.IsActive
}).ToList()
becomes:
StockItems = items.Select(s => new StockItemDto
{
Id = s.Id,
MaterialId = s.MaterialId,
MaterialName = s.MaterialName,
LengthInches = s.LengthInches,
LengthFormatted = s.LengthFormatted,
Name = s.Name,
Notes = s.Notes,
IsActive = s.IsActive
}).ToList()
- Step 3: Update
AddStockItem
[McpServerTool(Name = "add_stock_item"), Description("Adds a new stock item (a specific length of material that can be stocked).")]
public async Task<StockItemResult> AddStockItem(
[Description("Material ID (use list_materials to find IDs)")]
int materialId,
[Description("Stock length (e.g., '20'', '240', '20 ft')")]
string length,
[Description("Optional name/label for this stock item")]
string? name = null,
[Description("Initial quantity on hand (default 0)")]
int quantityOnHand = 0,
[Description("Notes")]
string? notes = null)
{
try
{
var stockItem = await _api.CreateStockItemAsync(materialId, length, name, quantityOnHand, notes);
if (stockItem == null)
return new StockItemResult { Success = false, Error = "Failed to create stock item" };
return new StockItemResult
{
Success = true,
StockItem = new StockItemDto
{
Id = stockItem.Id,
MaterialId = stockItem.MaterialId,
MaterialName = stockItem.MaterialName,
LengthInches = stockItem.LengthInches,
LengthFormatted = stockItem.LengthFormatted,
Name = stockItem.Name,
QuantityOnHand = stockItem.QuantityOnHand,
Notes = stockItem.Notes,
IsActive = stockItem.IsActive
}
};
}
catch (ApiConflictException ex)
{
return new StockItemResult { Success = false, Error = ex.Message };
}
catch (HttpRequestException ex)
{
return new StockItemResult { Success = false, Error = ex.Message };
}
}
becomes:
[McpServerTool(Name = "add_stock_item"), Description("Adds a new stock item (a specific length of material that can be stocked).")]
public async Task<StockItemResult> AddStockItem(
[Description("Material ID (use list_materials to find IDs)")]
int materialId,
[Description("Stock length (e.g., '20'', '240', '20 ft')")]
string length,
[Description("Optional name/label for this stock item")]
string? name = null,
[Description("Notes")]
string? notes = null)
{
try
{
var stockItem = await _api.CreateStockItemAsync(materialId, length, name, notes);
if (stockItem == null)
return new StockItemResult { Success = false, Error = "Failed to create stock item" };
return new StockItemResult
{
Success = true,
StockItem = new StockItemDto
{
Id = stockItem.Id,
MaterialId = stockItem.MaterialId,
MaterialName = stockItem.MaterialName,
LengthInches = stockItem.LengthInches,
LengthFormatted = stockItem.LengthFormatted,
Name = stockItem.Name,
Notes = stockItem.Notes,
IsActive = stockItem.IsActive
}
};
}
catch (ApiConflictException ex)
{
return new StockItemResult { Success = false, Error = ex.Message };
}
catch (HttpRequestException ex)
{
return new StockItemResult { Success = false, Error = ex.Message };
}
}
- Step 4: Update the
AddStockconvenience tool
[McpServerTool(Name = "add_stock"), Description("Convenience method: adds a material (if needed) and a stock item (if needed) with an initial quantity, all in one call.")]
public async Task<AddStockResult> AddStock(
[Description("Material shape (e.g., 'Angle', 'FlatBar')")]
string shape,
[Description("Material size (e.g., '2 x 2 x 1/4')")]
string size,
[Description("Stock length (e.g., '20'', '240')")]
string length,
[Description("Quantity on hand (default 0)")]
int quantityOnHand = 0,
[Description("Material type: Steel, Aluminum, Stainless, Brass, Copper (default: Steel)")]
string type = "Steel",
[Description("Grade or specification (e.g., 'A36', 'Hot Roll', '304', '6061-T6')")]
string? grade = null)
becomes:
[McpServerTool(Name = "add_stock"), Description("Convenience method: adds a material (if needed) and a stock item (if needed), all in one call.")]
public async Task<AddStockResult> AddStock(
[Description("Material shape (e.g., 'Angle', 'FlatBar')")]
string shape,
[Description("Material size (e.g., '2 x 2 x 1/4')")]
string size,
[Description("Stock length (e.g., '20'', '240')")]
string length,
[Description("Material type: Steel, Aluminum, Stainless, Brass, Copper (default: Steel)")]
string type = "Steel",
[Description("Grade or specification (e.g., 'A36', 'Hot Roll', '304', '6061-T6')")]
string? grade = null)
Further down in the same method, the two CreateStockItemAsync calls and the final result:
stockItem = await _api.CreateStockItemAsync(material.Id, length, null, quantityOnHand, null);
becomes:
stockItem = await _api.CreateStockItemAsync(material.Id, length, null, null);
return new AddStockResult
{
Success = true,
MaterialId = material.Id,
MaterialName = $"{material.Shape} - {material.Size}",
MaterialCreated = materialCreated,
StockItemId = stockItem.Id,
StockItemCreated = stockItemCreated,
LengthFormatted = ArchUnits.FormatFromInches(lengthInches),
QuantityOnHand = stockItem.QuantityOnHand
};
becomes:
return new AddStockResult
{
Success = true,
MaterialId = material.Id,
MaterialName = $"{material.Shape} - {material.Size}",
MaterialCreated = materialCreated,
StockItemId = stockItem.Id,
StockItemCreated = stockItemCreated,
LengthFormatted = ArchUnits.FormatFromInches(lengthInches)
};
- Step 5: Drop
QuantityOnHandfrom the two DTO classes at the bottom of the file
public class StockItemDto
{
public int Id { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public decimal LengthInches { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
public string? Name { get; set; }
public int QuantityOnHand { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
}
(this is a duplicate declaration in the "DTOs" region at the bottom of the file — same fix as Step 2, applied to that copy) becomes:
public class StockItemDto
{
public int Id { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public decimal LengthInches { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
public string? Name { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; }
}
Note:
InventoryTools.csdeclaresStockItemDtotwice today — once inline in the class (used byListStockItems/AddStockItem) and once in the#region DTOsblock at the bottom of the file. Both need theQuantityOnHandproperty removed; grep the file forpublic class StockItemDtoto confirm you've caught both before moving on.
public class AddStockResult
{
public bool Success { get; set; }
public string? Error { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public bool MaterialCreated { get; set; }
public int StockItemId { get; set; }
public bool StockItemCreated { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
public int QuantityOnHand { get; set; }
}
becomes:
public class AddStockResult
{
public bool Success { get; set; }
public string? Error { get; set; }
public int MaterialId { get; set; }
public string MaterialName { get; set; } = string.Empty;
public bool MaterialCreated { get; set; }
public int StockItemId { get; set; }
public bool StockItemCreated { get; set; }
public string LengthFormatted { get; set; } = string.Empty;
}
- Step 6: Build
Run:
dotnet build CutList.Mcp/CutList.Mcp.csproj
Expected: 0 errors.
- Step 7: Republish the MCP server
Per the project's MCP publishing workflow (CutList.Web must be running on http://localhost:5270 for a live smoke test, but publishing itself doesn't require it):
dotnet publish CutList.Mcp/CutList.Mcp.csproj -c Release -o "$USERPROFILE/.claude/mcp/CutList.Mcp"
- Step 8: Manual check
Restart the CutListMcp MCP connection (or restart this Claude Code session) and call add_stock with a shape/size/length that doesn't exist yet, confirming it still creates the material and stock item successfully without a quantityOnHand argument.
- Step 9: Commit
git add CutList.Mcp/ApiClient.cs CutList.Mcp/InventoryTools.cs
git commit -m "$(cat <<'EOF'
refactor: drop QuantityOnHand from CutList.Mcp inventory tools
Matches the REST API's StockItemDto/CreateStockItemDto shape after
inventory quantity tracking removal. Republished to
~/.claude/mcp/CutList.Mcp/.
EOF
)"
Task 6: Drop QuantityOnHand column and StockTransaction table
Files:
- Modify:
CutList.Web/Data/Entities/StockItem.cs - Delete:
CutList.Web/Data/Entities/StockTransaction.cs - Modify:
CutList.Web/Data/ApplicationDbContext.cs:16,160-177 - Create:
CutList.Web/Migrations/<timestamp>_RemoveInventoryQuantityTracking.cs(generated)
Interfaces:
-
Consumes: confirmation that nothing outside this set of files still references
StockItem.QuantityOnHand,StockTransaction, orStockTransactionType(true after Tasks 1-5). -
Produces: final entity/schema shape. No later task depends on anything new here beyond "the column/table are gone."
-
Step 1: Confirm no remaining references
Run:
mcp__RoslynBridge__search_code # query: "QuantityOnHand" across the solution
or, if unavailable, Grep for QuantityOnHand and StockTransaction across CutList.Web and CutList.Mcp. Expected: matches only inside CutList.Web/Data/Entities/StockItem.cs, CutList.Web/Data/Entities/StockTransaction.cs, CutList.Web/Data/ApplicationDbContext.cs, and the historical Migrations/*.cs/*.Designer.cs files (which are a permanent historical record and must not be edited).
- Step 2: Update
StockItem.cs
namespace CutList.Web.Data.Entities;
public class StockItem
{
public int Id { get; set; }
public int MaterialId { get; set; }
public decimal LengthInches { get; set; }
public string? Name { get; set; }
public int QuantityOnHand { get; set; } = 0;
public string? Notes { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public Material Material { get; set; } = null!;
public ICollection<StockTransaction> Transactions { get; set; } = new List<StockTransaction>();
}
becomes:
namespace CutList.Web.Data.Entities;
public class StockItem
{
public int Id { get; set; }
public int MaterialId { get; set; }
public decimal LengthInches { get; set; }
public string? Name { get; set; }
public string? Notes { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
public Material Material { get; set; } = null!;
}
- Step 3: Delete
StockTransaction.cs
Delete the file CutList.Web/Data/Entities/StockTransaction.cs entirely (it defined StockTransaction and StockTransactionType).
- Step 4: Update
ApplicationDbContext.cs
Remove the DbSet:
public DbSet<StockItem> StockItems => Set<StockItem>();
public DbSet<StockTransaction> StockTransactions => Set<StockTransaction>();
public DbSet<CuttingTool> CuttingTools => Set<CuttingTool>();
becomes:
public DbSet<StockItem> StockItems => Set<StockItem>();
public DbSet<CuttingTool> CuttingTools => Set<CuttingTool>();
Remove the StockTransaction config block:
// StockItem
modelBuilder.Entity<StockItem>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.LengthInches).HasPrecision(10, 4);
entity.Property(e => e.Name).HasMaxLength(100);
entity.Property(e => e.Notes).HasMaxLength(255);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
entity.HasOne(e => e.Material)
.WithMany(m => m.StockItems)
.HasForeignKey(e => e.MaterialId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(e => new { e.MaterialId, e.LengthInches }).IsUnique();
});
// StockTransaction
modelBuilder.Entity<StockTransaction>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Notes).HasMaxLength(500);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
entity.HasOne(e => e.StockItem)
.WithMany(s => s.Transactions)
.HasForeignKey(e => e.StockItemId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Job)
.WithMany()
.HasForeignKey(e => e.JobId)
.OnDelete(DeleteBehavior.SetNull);
});
// CuttingTool
becomes:
// StockItem
modelBuilder.Entity<StockItem>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.LengthInches).HasPrecision(10, 4);
entity.Property(e => e.Name).HasMaxLength(100);
entity.Property(e => e.Notes).HasMaxLength(255);
entity.Property(e => e.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
entity.HasOne(e => e.Material)
.WithMany(m => m.StockItems)
.HasForeignKey(e => e.MaterialId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(e => new { e.MaterialId, e.LengthInches }).IsUnique();
});
// CuttingTool
- Step 5: Build and check diagnostics
Run:
mcp__RoslynBridge__get_diagnostics_summary # for CutList.Web
dotnet build CutList.Web/CutList.Web.csproj
dotnet build CutList.sln
Expected: 0 errors across the whole solution — this is the point where every consumer touched in Tasks 1-5 finally has to line up.
- Step 6: Create and apply the EF Core migration
Run:
dotnet ef migrations add RemoveInventoryQuantityTracking --project CutList.Web
dotnet ef database update --project CutList.Web
Expected: the generated migration's Up drops the QuantityOnHand column from StockItems and drops the StockTransactions table (EF will infer this automatically from the model diff — no manual SQL needed). Confirm both operations appear in the generated migration file before applying.
- Step 7: Manual check
Run the full app end-to-end: create a job, add parts, add both a catalog stock item (finite quantity) and a custom unlimited stock line, optimize, confirm results are correct and the "Items Not Placed" section behaves as expected when quantity is insufficient. Confirm /stock CRUD still works post-migration.
- Step 8: Commit
git add CutList.Web/Data/Entities/StockItem.cs CutList.Web/Data/ApplicationDbContext.cs CutList.Web/Migrations/
git rm CutList.Web/Data/Entities/StockTransaction.cs
git commit -m "$(cat <<'EOF'
feat: drop QuantityOnHand column and StockTransactions table
Final step of removing inventory quantity tracking - see
docs/superpowers/specs/2026-08-01-remove-inventory-quantity-tracking-design.md.
Destructive to any existing on-hand/transaction data; confirmed
acceptable since nothing read it automatically.
EOF
)"
Task 7: Allow unlimited quantity for catalog-sourced job stock, and tweak the unplaced-items message
Files:
- Modify:
CutList.Web/Components/Pages/Jobs/Edit.razor
Interfaces:
-
Consumes:
JobStock.Quantity(unchanged type —int,-1= unlimited, already supported end-to-end byCutListPackingService/MultiBinEnginefor custom-length rows since before this refactor). -
Produces: nothing new for other tasks; this is the last functional change in the plan.
-
Step 1: Widen the "Qty to Use" input on the catalog-stock form
<div class="col-md-3">
<label class="form-label">Qty to Use</label>
<input type="number" class="form-control" @bind="newStock.Quantity" min="1" />
</div>
becomes:
<div class="col-md-3">
<label class="form-label">Qty to Use</label>
<input type="number" class="form-control" @bind="newStock.Quantity" min="-1" />
<small class="text-muted">-1 = unlimited</small>
</div>
- Step 2: Match the validation rule to the custom-stock form
In SaveStockFromInventoryAsync:
if (newStock.Quantity < 1)
{
stockErrorMessage = "Quantity must be at least 1";
return;
}
becomes:
if (newStock.Quantity < -1 || newStock.Quantity == 0)
{
stockErrorMessage = "Quantity must be at least 1 (or -1 for unlimited)";
return;
}
- Step 3: Tweak the "Items Not Placed" copy on the Results tab
@if (materialResult.PackResult.ItemsNotUsed.Count > 0)
{
<div class="alert alert-danger">
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> —
No stock lengths available or parts too long.
</div>
}
becomes:
@if (materialResult.PackResult.ItemsNotUsed.Count > 0)
{
<div class="alert alert-danger">
<strong>@materialResult.PackResult.ItemsNotUsed.Count items not placed</strong> —
not enough stock quantity entered for this job, no stock lengths configured, or parts too long.
</div>
}
- Step 4: Build and check diagnostics
Run:
mcp__RoslynBridge__get_diagnostics_summary # for CutList.Web
dotnet build CutList.Web/CutList.Web.csproj
Expected: 0 errors.
- Step 5: Manual check
In the running app, add a catalog-sourced stock line to a job's Stock tab and set its quantity to -1; confirm it saves without the old "at least 1" error. Set a finite quantity below what's needed to pack all parts and confirm the Results tab's unplaced-items message reads correctly.
- Step 6: Commit
git add CutList.Web/Components/Pages/Jobs/Edit.razor
git commit -m "$(cat <<'EOF'
feat: allow unlimited quantity for catalog-sourced job stock
Custom-length job stock already supported -1 (unlimited); catalog-
sourced stock only allowed >= 1. Closes that inconsistency per
docs/superpowers/specs/2026-08-01-remove-inventory-quantity-tracking-design.md.
Also updates the Results tab's unplaced-items message now that
insufficient configured quantity is a normal cause, not an edge case.
EOF
)"
Task 8: Full-solution verification pass
Files: none (verification only)
Interfaces:
-
Consumes: everything from Tasks 1-7.
-
Produces: confidence the refactor is complete and consistent.
-
Step 1: Clean full-solution build
dotnet clean CutList.sln
dotnet build CutList.sln
Expected: 0 errors, 0 new warnings related to StockItem/StockTransaction/QuantityOnHand.
- Step 2: Confirm no stray references remain
mcp__RoslynBridge__search_code # query: "QuantityOnHand" and separately "StockTransaction" across the whole solution
Expected: zero hits outside historical Migrations/*.cs / *.Designer.cs files (which must never be edited retroactively).
- Step 3: End-to-end manual walkthrough
With dotnet run --project CutList.Web/CutList.Web.csproj running:
/stock— create two stock items for the same material (different lengths), confirm no quantity fields anywhere on the page.- Create a new job, add parts for that material.
- On the Stock tab: add one catalog-sourced stock row with a finite quantity (e.g.
2), and one custom-length row set to unlimited (-1). - Optimize. Confirm
InStockBinsreflects the catalog row correctly, and parts beyond what the finite quantity can supply either come from the unlimited custom row or land in "Items Not Placed" if none is configured for that case. - Lock the job, confirm locking still works exactly as before (unaffected by this refactor).
- Via the MCP tools (
list_stock_items,add_stock), confirm both work end-to-end against the running app with noquantityOnHandinvolved.
- Step 4: Update CLAUDE.md if needed
Re-read the StockItem, StockTransaction (delete this subsection), StockItemService, and packing-related sections of CutList.Web's CLAUDE.md entity/service tables and correct any that still describe QuantityOnHand/StockTransaction/auto-discovery. This project's CLAUDE.md explicitly requires staying current with entity/service changes.
- Step 5: Final commit (if Step 4 produced changes)
git add CLAUDE.md
git commit -m "$(cat <<'EOF'
docs: update CLAUDE.md for inventory quantity tracking removal
EOF
)"
Self-Review Notes
- Spec coverage: every "Scope" bullet in
docs/superpowers/specs/2026-08-01-remove-inventory-quantity-tracking-design.mdmaps to a task above — data model (Task 6), services/API (Tasks 2, 3), catalog (Task 4), MCP (Task 5), UI (Tasks 1, 7), migration (Task 6). "Out of scope" items (packing algorithm itself, Purchase List concept, seed-file cleanup) are correctly untouched by every task. - Placeholder scan: no TBDs; every step shows exact before/after code or an exact shell command.
- Type consistency:
JobStock.Quantitystaysintthroughout (no type change, just widened validation range) — verified consistent across Tasks 1, 3, 7.StockItemDto/CreateStockItemDtoshapes in Task 2 match what Task 5's MCP client is updated to expect.