Initial commit: JDE Scoping Tool migration project

Set up repository with legacy .NET Framework 4.8 source (OLD/),
new .NET 10 Blazor solution (NEW/), OpenSpec specifications,
documentation, and project configuration.
This commit is contained in:
Joseph Doherty
2026-01-02 07:43:29 -05:00
commit 26ff8d9b4f
1761 changed files with 596509 additions and 0 deletions
@@ -0,0 +1,518 @@
# Search Processing Design
## Overview
This document describes the architecture and implementation approach for the search processing subsystem, including the SqlKata query builder pattern, filter handler architecture, and search processor service.
## Architecture
### High-Level Component Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ SearchProcessor │
│ - Orchestrates search execution │
│ - Coordinates filter enrichment, query building, execution │
└────────────────────────────────┬────────────────────────────────────────┘
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ ILotFinder │ │ ISearchQuery │ │ IWorkOrder │
│ Repository │ │ Builder │ │ TraversalService│
│ (enrichment) │ │ (SQL generation)│ │ (downstream) │
└─────────────────┘ └────────┬────────┘ └─────────────────┘
┌───────────┴───────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Filter Handlers │ │ SqlServerCompiler│
│ (composable) │ │ (SQL output) │
└─────────────────┘ └─────────────────┘
```
### Project Structure
```
NEW/src/JdeScoping.SearchProcessing/
├── Interfaces/
│ ├── ISearchProcessor.cs
│ ├── ISearchQueryBuilder.cs
│ ├── IFilterHandler.cs
│ └── IWorkOrderTraversalService.cs
├── QueryBuilders/
│ ├── SqlKataSearchQueryBuilder.cs
│ └── MisQueryBuilder.cs
├── FilterHandlers/
│ ├── FilterHandlerBase.cs
│ ├── WorkOrderFilterHandler.cs
│ ├── ItemNumberFilterHandler.cs
│ ├── ProfitCenterFilterHandler.cs
│ ├── WorkCenterFilterHandler.cs
│ ├── OperatorFilterHandler.cs
│ ├── ComponentLotFilterHandler.cs
│ ├── ItemOperationMisFilterHandler.cs
│ └── TimespanFilterHandler.cs
├── Models/
│ ├── SearchModel.cs
│ ├── SearchQueryResult.cs
│ ├── FilterEntries/
│ │ ├── WorkOrderFilterEntry.cs
│ │ ├── ItemNumberFilterEntry.cs
│ │ ├── ProfitCenterFilterEntry.cs
│ │ ├── WorkCenterFilterEntry.cs
│ │ ├── OperatorFilterEntry.cs
│ │ ├── ComponentLotFilterEntry.cs
│ │ └── ItemOperationMisFilterEntry.cs
│ └── Results/
│ ├── SearchResult.cs
│ ├── MisSearchResult.cs
│ └── MisNonMatchSearchResult.cs
├── Services/
│ ├── SearchProcessor.cs
│ └── WorkOrderTraversalService.cs
├── Configuration/
│ └── SearchProcessingOptions.cs
├── Attributes/
│ ├── OutputColumnAttribute.cs
│ └── OutputTableAttribute.cs
├── Extensions/
│ ├── SearchModelExtensions.cs
│ └── TableValuedParameterExtensions.cs
├── ServiceCollectionExtensions.cs
└── JdeScoping.SearchProcessing.csproj
```
## SqlKata Query Builder Architecture
### ISearchQueryBuilder Interface
```csharp
public interface ISearchQueryBuilder
{
/// <summary>
/// Builds the main search query for flagging and retrieving work orders.
/// </summary>
SearchQueryResult BuildSearchQuery(SearchModel model);
/// <summary>
/// Builds the MIS data extraction query when ExtractMisData is enabled.
/// </summary>
SearchQueryResult BuildMisQuery(SearchModel model);
/// <summary>
/// Builds the MIS non-match query for work orders without MIS records.
/// </summary>
SearchQueryResult BuildMisNonMatchQuery(SearchModel model);
}
public record SearchQueryResult(
string Sql,
IDictionary<string, object> Parameters,
IReadOnlyList<string> TempTableSetupSql);
```
### SqlKata Integration Pattern
The SqlKata query builder composes queries using fluent methods:
```csharp
public sealed class SqlKataSearchQueryBuilder : ISearchQueryBuilder
{
private readonly SqlServerCompiler _compiler = new();
private readonly IEnumerable<IFilterHandler> _filterHandlers;
public SqlKataSearchQueryBuilder(IEnumerable<IFilterHandler> filterHandlers)
{
_filterHandlers = filterHandlers;
}
public SearchQueryResult BuildSearchQuery(SearchModel model)
{
var setupStatements = new List<string>();
var parameters = new Dictionary<string, object>();
// Build temp table setup SQL
setupStatements.Add(BuildTempWoTableSql());
// Apply filter handlers (each may add setup SQL and parameters)
foreach (var handler in _filterHandlers.Where(h => h.IsEnabled(model)))
{
var filterResult = handler.Apply(model, _compiler);
setupStatements.AddRange(filterResult.SetupSql);
foreach (var param in filterResult.Parameters)
{
parameters[param.Key] = param.Value;
}
}
// Build final result query
var resultQuery = BuildResultQuery();
var compiled = _compiler.Compile(resultQuery);
return new SearchQueryResult(
compiled.Sql,
MergeParameters(parameters, compiled.NamedBindings),
setupStatements);
}
}
```
### Why SqlKata Instead of T4 Templates
| Aspect | T4 Template (Legacy) | SqlKata (New) |
|--------|---------------------|---------------|
| Testability | Cannot unit test | Test query structure without DB |
| Type Safety | String concatenation | Fluent API with IntelliSense |
| SQL Injection | Manual parameter handling | Parameterized by default |
| Maintenance | Edit .tt file, regenerate | Edit C# code directly |
| SDK Support | Limited in modern .NET | Full .NET 10 support |
| Composability | Monolithic template | Pluggable filter handlers |
## Filter Handler Pattern
### IFilterHandler Interface
```csharp
public interface IFilterHandler
{
/// <summary>
/// Determines if this filter is active for the given search model.
/// </summary>
bool IsEnabled(SearchModel model);
/// <summary>
/// Applies the filter, returning setup SQL and parameters.
/// </summary>
FilterResult Apply(SearchModel model, SqlServerCompiler compiler);
/// <summary>
/// Priority for handler execution order (lower = earlier).
/// </summary>
int Priority { get; }
}
public record FilterResult(
IReadOnlyList<string> SetupSql,
IDictionary<string, object> Parameters);
```
### Filter Handler Implementations
Each filter handler encapsulates the logic for one filter type:
#### WorkOrderFilterHandler
```csharp
public sealed class WorkOrderFilterHandler : FilterHandlerBase
{
public override int Priority => 10;
public override bool IsEnabled(SearchModel model)
=> model.WorkOrderFilterEnabled;
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
{
// Generates MERGE into #Temp_WO with ManuallySpecified = 1
// Followed by split order detection
var sql = BuildWorkOrderMergeSql();
var parameters = new Dictionary<string, object>
{
["p_WorkOrderFilter"] = model.CreateWorkOrderFilterParameter()
};
return new FilterResult(new[] { sql }, parameters);
}
}
```
#### ComponentLotFilterHandler
```csharp
public sealed class ComponentLotFilterHandler : FilterHandlerBase
{
public override int Priority => 30;
public override bool IsEnabled(SearchModel model)
=> model.ComponentLotFilterEnabled;
public override FilterResult Apply(SearchModel model, SqlServerCompiler compiler)
{
// Joins Lot -> WorkOrderComponent/LotUsage -> WorkOrder
// Sets CARDEX = 1 flag
var sql = BuildComponentLotMergeSql();
var parameters = new Dictionary<string, object>
{
["p_ComponentLotFilter"] = model.CreateComponentLotFilterParameter()
};
return new FilterResult(new[] { sql }, parameters);
}
}
```
### Handler Execution Order
Handlers execute in priority order to ensure dependent temp tables exist:
| Priority | Handler | Creates/Uses |
|----------|---------|--------------|
| 10 | WorkOrderFilterHandler | Creates #Temp_WO entries |
| 20 | ItemNumberFilterHandler | Creates #P_ItemNumbers |
| 30 | ComponentLotFilterHandler | Uses Lot, creates #Temp_WO entries |
| 40 | ProfitCenterFilterHandler | Creates #P_WorkCenters |
| 50 | WorkCenterFilterHandler | Creates #P_WorkCenters |
| 60 | OperatorFilterHandler | Creates #P_OperatorIDs |
| 70 | ItemOperationMisFilterHandler | Creates #P_PartOperations |
| 80 | TimespanFilterHandler | Adds WHERE clause |
## IAsyncEnumerable Streaming Pattern
### Large Result Set Handling
For searches returning thousands of work orders, streaming avoids loading all results into memory:
```csharp
public interface ISearchProcessor
{
/// <summary>
/// Executes search and returns results as async stream.
/// </summary>
IAsyncEnumerable<SearchResult> ExecuteSearchAsync(
SearchModel model,
CancellationToken ct = default);
/// <summary>
/// Executes search and materializes all results into SearchModel.
/// </summary>
Task<SearchModel> ExecuteSearchToModelAsync(
SearchModel model,
CancellationToken ct = default);
}
```
### Streaming Implementation
```csharp
public sealed class SearchProcessor : ISearchProcessor
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly ISearchQueryBuilder _queryBuilder;
private readonly IWorkOrderTraversalService _traversalService;
private readonly ILogger<SearchProcessor> _logger;
public async IAsyncEnumerable<SearchResult> ExecuteSearchAsync(
SearchModel model,
[EnumeratorCancellation] CancellationToken ct = default)
{
await using var connection = await _connectionFactory
.CreateLotFinderConnectionAsync(ct);
// Execute setup SQL (temp tables, filter population)
var queryResult = _queryBuilder.BuildSearchQuery(model);
foreach (var setupSql in queryResult.TempTableSetupSql)
{
await connection.ExecuteAsync(setupSql, queryResult.Parameters);
}
// Stream results
await foreach (var result in connection.QueryUnbufferedAsync<SearchResult>(
queryResult.Sql,
queryResult.Parameters)
.WithCancellation(ct))
{
yield return result;
}
}
}
```
## Downstream Work Order Traversal
### Stored Procedure Approach
The iterative traversal logic (up to 20 iterations finding downstream work orders) is better suited to a stored procedure:
```csharp
public interface IWorkOrderTraversalService
{
/// <summary>
/// Traverses downstream work orders via stored procedure.
/// Called after initial filtering to find related work orders.
/// </summary>
Task TraverseDownstreamAsync(
SqlConnection connection,
CancellationToken ct = default);
}
```
### Why Stored Procedure
- **Iterative logic**: WHILE loops with temp table operations are efficient in T-SQL
- **Reduced round trips**: Single call instead of 20+ iterations from C#
- **Transaction scope**: All MERGE operations in same transaction
- **Legacy compatibility**: Mirrors existing QueryTemplate.tt behavior
## Table-Valued Parameter Helpers
### Extension Methods
```csharp
public static class TableValuedParameterExtensions
{
public static SqlMapper.ICustomQueryParameter CreateWorkOrderFilterParameter(
this SearchModel model)
{
var dataTable = new DataTable();
dataTable.Columns.Add("WorkOrderNumber", typeof(long));
foreach (var entry in model.WorkOrderFilter)
{
dataTable.Rows.Add(entry.WorkOrderNumber);
}
return dataTable.AsTableValuedParameter("WorkOrderFilterParameter");
}
// Similar methods for all 7 filter types...
}
```
### TVP Type Mapping
| C# Method | SQL Server Type | Columns |
|-----------|-----------------|---------|
| `CreateWorkOrderFilterParameter` | `WorkOrderFilterParameter` | `WorkOrderNumber BIGINT` |
| `CreateItemNumberFilterParameter` | `ItemNumberFilterParameter` | `ItemNumber VARCHAR(25)` |
| `CreateProfitCenterFilterParameter` | `ProfitCenterFilterParameter` | `Code VARCHAR(12)` |
| `CreateWorkCenterFilterParameter` | `WorkCenterFilterParameter` | `Code VARCHAR(12)` |
| `CreateOperatorFilterParameter` | `OperatorFilterParameter` | `UserName VARCHAR(10)` |
| `CreateComponentLotFilterParameter` | `ComponentLotFilterParameter` | `ComponentLotNumber VARCHAR(30), ItemNumber VARCHAR(25)` |
| `CreateItemOperationMisFilterParameter` | `ItemOperationMisFilterParameter` | `ItemNumber, OperationNumber, MisNumber, MisRevision` |
## Configuration Options
### SearchProcessingOptions Class
```csharp
public class SearchProcessingOptions
{
public const string SectionName = "SearchProcessing";
/// <summary>
/// Query timeout in seconds for search execution.
/// </summary>
public int QueryTimeoutSeconds { get; set; } = 600;
/// <summary>
/// Maximum downstream traversal iterations.
/// </summary>
public int MaxTraversalIterations { get; set; } = 20;
/// <summary>
/// Enable debug SQL logging.
/// </summary>
public bool EnableDebugSql { get; set; } = false;
/// <summary>
/// Path to write debug SQL files (when EnableDebugSql is true).
/// </summary>
public string? DebugSqlPath { get; set; }
}
```
### Configuration Binding
```json
{
"SearchProcessing": {
"QueryTimeoutSeconds": 600,
"MaxTraversalIterations": 20,
"EnableDebugSql": false,
"DebugSqlPath": null
}
}
```
## Service Registration
### AddSearchProcessing Extension Method
```csharp
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSearchProcessing(
this IServiceCollection services,
IConfiguration configuration)
{
// Bind options
services.Configure<SearchProcessingOptions>(
configuration.GetSection(SearchProcessingOptions.SectionName));
// Register SqlKata compiler (singleton, thread-safe)
services.AddSingleton<SqlServerCompiler>();
// Register filter handlers (scoped)
services.AddScoped<IFilterHandler, WorkOrderFilterHandler>();
services.AddScoped<IFilterHandler, ItemNumberFilterHandler>();
services.AddScoped<IFilterHandler, ProfitCenterFilterHandler>();
services.AddScoped<IFilterHandler, WorkCenterFilterHandler>();
services.AddScoped<IFilterHandler, OperatorFilterHandler>();
services.AddScoped<IFilterHandler, ComponentLotFilterHandler>();
services.AddScoped<IFilterHandler, ItemOperationMisFilterHandler>();
services.AddScoped<IFilterHandler, TimespanFilterHandler>();
// Register query builders (scoped)
services.AddScoped<ISearchQueryBuilder, SqlKataSearchQueryBuilder>();
// Register services (scoped)
services.AddScoped<ISearchProcessor, SearchProcessor>();
services.AddScoped<IWorkOrderTraversalService, WorkOrderTraversalService>();
return services;
}
}
```
## Testing Strategy
### Unit Tests
- **Query Builder Tests**: Verify generated SQL structure without executing
- **Filter Handler Tests**: Test each handler in isolation
- **Parameter Tests**: Verify TVP creation for all filter types
- **Mock Repository**: Use NSubstitute for `ILotFinderRepository`
### Integration Tests
- **Full Search Flow**: Execute search against test database
- **Filter Combinations**: Matrix of filter permutations
- **Large Result Sets**: Verify streaming behavior
- **MIS Extraction**: Test with and without MIS data
### Test Frameworks
- **xUnit**: Test framework
- **Shouldly**: Fluent assertions
- **NSubstitute**: Mocking framework
## NuGet Dependencies
```xml
<ItemGroup>
<PackageReference Include="SqlKata" Version="3.0.*" />
<PackageReference Include="SqlKata.Execution" Version="3.0.*" />
<PackageReference Include="Dapper" Version="2.1.*" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.*" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.*" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.*" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Test'">
<PackageReference Include="xunit" Version="2.9.*" />
<PackageReference Include="Shouldly" Version="4.2.*" />
<PackageReference Include="NSubstitute" Version="5.1.*" />
</ItemGroup>
```
@@ -0,0 +1,98 @@
# Implement Search Processing
## Summary
Implement the search processing subsystem that executes user-defined filter queries against the SQL Server cache database. This phase replaces the legacy T4 text template (`QueryTemplate.tt`) with a SqlKata fluent query builder, providing type-safe query construction, parameterized SQL generation, and composable filter handling.
## Scope
### In Scope
- `ISearchQueryBuilder` interface and `SqlKataSearchQueryBuilder` implementation
- Filter handler pattern with individual handlers per filter type:
- `WorkOrderFilterHandler`
- `ItemNumberFilterHandler`
- `ProfitCenterFilterHandler`
- `WorkCenterFilterHandler`
- `OperatorFilterHandler`
- `ComponentLotFilterHandler`
- `ItemOperationMisFilterHandler`
- `TimespanFilterHandler`
- `ISearchProcessor` interface and `SearchProcessor` service implementation
- `IWorkOrderTraversalService` interface for downstream work order traversal
- `SearchModel` class (reporting model with enriched filter entries)
- Filter entry record types (immutable DTOs with output attributes)
- Result record types: `SearchResult`, `MisSearchResult`, `MisNonMatchSearchResult`
- Table-valued parameter creation helpers
- MIS data extraction query building
- `SearchProcessingOptions` configuration class
- `AddSearchProcessing` service registration extension method
- Unit tests with xUnit, Shouldly, and NSubstitute
### Out of Scope
- Background job scheduling (handled by Phase 5 data-sync or separate worker phase)
- Excel export generation (Phase 7: excel-export)
- API endpoints for search submission (Phase 8: web-api-auth)
- SignalR real-time status updates (Phase 8: web-api-auth)
- Database schema changes (Phase 1: migrate-database-schema)
## Motivation
The legacy T4 text template approach has significant limitations:
- **SDK incompatibility**: T4 templates are poorly supported in modern .NET SDK-style projects
- **Untestable**: Generated SQL cannot be unit tested without database execution
- **Fragile**: String concatenation prone to SQL injection and syntax errors
- **Untyped**: No compile-time validation of query structure
SqlKata provides:
- **Fluent API**: Readable, composable query building with IntelliSense support
- **Parameterized by default**: SQL injection protection built-in
- **Testable**: Unit test query building without database
- **Type-safe**: Compile-time checking of method calls
- **SQL Server optimized**: SqlServerCompiler generates optimized T-SQL
## Acceptance Criteria
1. `ISearchQueryBuilder` interface defined with `BuildSearchQuery(SearchModel)` method
2. `SqlKataSearchQueryBuilder` generates equivalent SQL to legacy `QueryTemplate.tt`
3. All 8 filter handlers implemented with conditional join/where clause generation
4. `SearchProcessor` orchestrates:
- Filter entry enrichment via repository lookups
- Query building via SqlKata
- Query execution via Dapper
- Result aggregation into `SearchModel.Results`
- MIS data extraction when `ExtractMisData = true`
5. Downstream work order traversal calls stored procedure `dbo.TraverseWorkOrders`
6. Table-valued parameters created correctly for all filter types
7. All result types include `OutputColumnAttribute` and `OutputTableAttribute` for Excel export
8. Unit tests verify:
- Query builder generates expected SQL structure
- Filter handlers apply correct joins and conditions
- Parameter binding works for all TVP types
9. `AddSearchProcessing` registers all services with appropriate lifetimes
10. `openspec validate implement-search-processing --strict` passes
## Dependencies
- **Phase 1**: migrate-database-schema - Database tables, TVP types, stored procedures
- **Phase 3**: implement-domain-models - Core domain entities (Search, SearchCriteria, etc.)
- **Phase 4**: implement-data-access - `IDbConnectionFactory`, `ILotFinderRepository` for lookups
- **NuGet packages**: `SqlKata`, `SqlKata.Execution`, `Dapper`, `Microsoft.Data.SqlClient`
## Risks
| Risk | Mitigation |
|------|------------|
| Query logic drift from legacy | Codex MCP review comparing generated SQL against QueryTemplate.tt output |
| Complex filter combinations | Comprehensive unit test matrix covering all filter permutations |
| MIS extraction query complexity | Retain MIS extraction as separate stored procedure if needed |
| Performance regression | Benchmark query execution time against legacy implementation |
| Downstream traversal correctness | Stored procedure `dbo.TraverseWorkOrders` encapsulates iterative logic |
## Related Specs
- `search-processing` - Primary specification for query building and search execution
- `domain-models` - Entity types used in search criteria and results
- `data-access` - Repository interfaces for filter enrichment lookups
- `database-schema` - SQL Server tables and TVP types for search execution
@@ -0,0 +1,272 @@
# Search Processing Specification Delta
## Purpose
This document captures ADDED and MODIFIED requirements for the search processing subsystem specific to the .NET 10 migration. It supplements the base specification at `openspec/specs/search-processing/spec.md`.
## ADDED Requirements
### Requirement: SqlKata Query Builder Integration
The system SHALL use SqlKata fluent query builder instead of T4 text templates for dynamic SQL generation.
#### Inputs
- `SearchModel` containing filter criteria and enriched filter entries
- `SqlServerCompiler` for T-SQL generation
#### Outputs
- `SearchQueryResult` record containing:
- `Sql`: Parameterized T-SQL query string
- `Parameters`: Dictionary of named parameter values
- `TempTableSetupSql`: List of temp table creation/population statements
#### Business Rules
- SqlKata `SqlServerCompiler` SHALL be registered as a singleton (thread-safe)
- All queries SHALL use named parameters (not positional)
- Parameter names SHALL match legacy convention (`@p_*` prefix)
- Generated SQL SHALL produce equivalent results to legacy QueryTemplate.tt
#### Scenario: Build query with SqlKata
- **WHEN** `ISearchQueryBuilder.BuildSearchQuery(model)` is called
- **THEN** SqlKata generates parameterized SQL with named bindings
- **AND** the `SearchQueryResult.Sql` is executable via Dapper
- **AND** `SearchQueryResult.Parameters` contains all TVP parameters
---
### Requirement: Filter Handler Pattern
The system SHALL use a composable filter handler pattern for modular query building.
#### Inputs
- `SearchModel` with active filter criteria
- `SqlServerCompiler` for SQL compilation
#### Outputs
- `FilterResult` record containing:
- `SetupSql`: List of temp table setup statements
- `Parameters`: Dictionary of parameters for this filter
#### Business Rules
- Each filter type SHALL have a dedicated `IFilterHandler` implementation
- Filter handlers SHALL be registered in dependency injection container
- Handlers SHALL execute in priority order (lower priority = earlier execution)
- Handler priorities SHALL ensure dependent temp tables exist before use:
- WorkOrder: 10
- ItemNumber: 20
- ComponentLot: 30
- ProfitCenter: 40
- WorkCenter: 50
- Operator: 60
- ItemOperationMis: 70
- Timespan: 80
#### Scenario: Execute filter handlers in order
- **WHEN** search criteria includes work orders, items, and operators
- **THEN** WorkOrderFilterHandler executes first (priority 10)
- **AND** ItemNumberFilterHandler executes second (priority 20)
- **AND** OperatorFilterHandler executes later (priority 60)
---
### Requirement: IAsyncEnumerable Result Streaming
The system SHALL support streaming large result sets using `IAsyncEnumerable<T>`.
#### Inputs
- `SearchModel` with executed query
- `CancellationToken` for cooperative cancellation
#### Outputs
- `IAsyncEnumerable<SearchResult>` streaming results one at a time
#### Business Rules
- Streaming SHALL use Dapper's `QueryUnbufferedAsync` method
- Cancellation SHALL be supported via `[EnumeratorCancellation]` attribute
- Memory allocation SHALL remain constant regardless of result set size
- Consumer MAY materialize results using `ToListAsync()` when needed
#### Scenario: Stream large result set
- **WHEN** search returns 10,000 work orders
- **THEN** results stream via `IAsyncEnumerable<SearchResult>`
- **AND** memory usage remains constant during enumeration
- **AND** `await foreach` consumes results incrementally
---
### Requirement: Async-First Design
The system SHALL use async methods throughout the search processing pipeline.
#### Business Rules
- All repository methods SHALL accept `CancellationToken` parameter
- All database operations SHALL use async Dapper methods (`QueryAsync`, `ExecuteAsync`)
- Long-running operations SHALL respect cancellation tokens
- `ISearchProcessor.ExecuteSearchAsync` SHALL be the primary entry point
#### Scenario: Cancel long-running search
- **WHEN** a search is in progress and cancellation is requested
- **THEN** the operation throws `OperationCanceledException`
- **AND** database connections are properly disposed
---
### Requirement: Configuration via IOptions Pattern
The system SHALL use `IOptions<SearchProcessingOptions>` for configuration.
#### Inputs
- `appsettings.json` section: `SearchProcessing`
#### Outputs
- Strongly-typed `SearchProcessingOptions` injected via DI
#### Configuration Properties
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `QueryTimeoutSeconds` | int | 600 | SQL query timeout |
| `MaxTraversalIterations` | int | 20 | Downstream traversal limit |
| `EnableDebugSql` | bool | false | Write SQL to debug files |
| `DebugSqlPath` | string? | null | Path for debug SQL files |
#### Scenario: Configure query timeout
- **WHEN** `SearchProcessingOptions.QueryTimeoutSeconds` is set to 900
- **THEN** Dapper queries use `commandTimeout: 900`
---
### Requirement: Service Registration Extension
The system SHALL provide an `AddSearchProcessing` extension method for DI registration.
#### Service Lifetimes
| Service | Lifetime | Rationale |
|---------|----------|-----------|
| `SqlServerCompiler` | Singleton | Thread-safe, stateless |
| `IFilterHandler` implementations | Scoped | Per-request state |
| `ISearchQueryBuilder` | Scoped | Uses scoped handlers |
| `ISearchProcessor` | Scoped | Uses scoped repositories |
| `IWorkOrderTraversalService` | Scoped | Uses connection factory |
#### Scenario: Register search processing services
- **WHEN** `services.AddSearchProcessing(configuration)` is called
- **THEN** all search processing services are registered
- **AND** `ISearchProcessor` can be resolved from service provider
---
## MODIFIED Requirements
### Requirement: Work Order Traversal
The system SHALL execute downstream work order traversal via stored procedure `dbo.TraverseWorkOrders` instead of inline WHILE loop in generated SQL.
#### Inputs
- Active database connection with `#Temp_WO` temporary table populated
- Maximum iteration count (default 20)
#### Outputs
- Updated `#Temp_WO` table with downstream work orders flagged
#### Business Rules
- `IWorkOrderTraversalService.TraverseDownstreamAsync` SHALL call stored procedure
- Stored procedure SHALL contain iterative WHILE loop logic
- Single stored procedure call SHALL replace 20 inline iterations
- Transaction scope SHALL be maintained within stored procedure
#### Scenario: Execute downstream traversal via stored procedure
- **WHEN** initial work orders are flagged in `#Temp_WO`
- **THEN** `dbo.TraverseWorkOrders` stored procedure is called
- **AND** downstream work orders are added with PartsList and CARDEX flags
- **AND** split orders are detected and flagged
---
### Requirement: Filter Entry Types
The system SHALL use C# record types for filter entry DTOs to provide immutability and value semantics.
#### Inputs
- Raw filter values from `SearchCriteria`
- Reference data lookups from `ILotFinderRepository`
#### Outputs
- Immutable record instances with output attributes for Excel export
#### Business Rules
- All filter entry types SHALL be declared as C# records
- Records SHALL use primary constructor syntax
- Output attributes SHALL be applied using `[property:]` target
- Records SHALL provide value-based equality for testing
#### Scenario: Create immutable filter entry record
- **WHEN** a WorkOrderFilterEntry is created with WorkOrderNumber 12345 and ItemNumber "ITEM-001"
- **THEN** the record is immutable (properties are init-only)
- **AND** two records with same values are considered equal
---
### Requirement: SQL Client Package
The system SHALL use `Microsoft.Data.SqlClient` instead of deprecated `System.Data.SqlClient` for SQL Server connectivity.
#### Business Rules
- All SQL Server connections SHALL use `Microsoft.Data.SqlClient.SqlConnection`
- All SQL commands SHALL use `Microsoft.Data.SqlClient.SqlCommand`
- NuGet package `Microsoft.Data.SqlClient` version 5.2+ SHALL be referenced
- Code SHALL NOT reference `System.Data.SqlClient` namespace
#### Scenario: Create SQL connection with modern client
- **WHEN** `IDbConnectionFactory.CreateLotFinderConnectionAsync` is called
- **THEN** a `Microsoft.Data.SqlClient.SqlConnection` instance is returned
- **AND** connection supports all modern SQL Server features
---
## Migration Notes
| Legacy Pattern | New Pattern | Status |
|----------------|-------------|--------|
| T4 Text Template | SqlKata fluent builder | ADDED |
| Inline WHILE loop | Stored procedure | MODIFIED |
| Filter entry classes | Record types | MODIFIED |
| Synchronous Dapper | Async Dapper | MODIFIED |
| System.Data.SqlClient | Microsoft.Data.SqlClient | MODIFIED |
| Static class methods | DI-registered services | MODIFIED |
| Newtonsoft.Json | System.Text.Json | Retained in domain models |
---
## Open Questions
None - all design decisions resolved per base specification.
@@ -0,0 +1,306 @@
# Tasks: Implement Search Processing
## Phase 1: Project Setup
- [x] 001: Create JdeScoping.SearchProcessing project
- Location: `NEW/src/JdeScoping.SearchProcessing/JdeScoping.SearchProcessing.csproj`
- Dependencies: SqlKata, SqlKata.Execution, Dapper, Microsoft.Data.SqlClient
- Validation: `dotnet build` succeeds
- [x] 002: Add project reference to JdeScoping.Host
- Location: `NEW/src/JdeScoping.Host/JdeScoping.Host.csproj`
- Validation: Solution builds with new reference
- [x] 003: Create SearchProcessingOptions configuration class
- Location: `NEW/src/JdeScoping.SearchProcessing/Configuration/SearchProcessingConfiguration.cs`
- Properties: QueryTimeoutSeconds, MaxTraversalIterations, EnableDebugSql, DebugSqlPath
- Validation: Options bind from appsettings.json
## Phase 2: Output Attributes
- [x] 004: Create OutputColumnAttribute
- Location: `NEW/src/JdeScoping.SearchProcessing/Attributes/OutputColumnAttribute.cs`
- Properties: Order, HeaderText, Format, AutoWidth, Width, WrapText
- Constants: DATE_FORMAT, TIMESTAMP_FORMAT, WRAPPED_COLUMN_WIDTH
- Validation: Attribute compiles and is applicable to properties
- [x] 005: Create OutputTableAttribute
- Location: `NEW/src/JdeScoping.SearchProcessing/Attributes/OutputTableAttribute.cs`
- Properties: TabName, TableName, ShowHeader
- Validation: Attribute compiles and is applicable to classes/records
## Phase 3: Filter Entry Records
- [x] 006: Create WorkOrderFilterEntry record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/FilterEntries/WorkOrderFilterEntry.cs`
- Properties: WorkOrderNumber (long), ItemNumber (string)
- Include OutputTable and OutputColumn attributes
- Validation: Record compiles with proper attributes
- [x] 007: Create ItemNumberFilterEntry record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/FilterEntries/ItemNumberFilterEntry.cs`
- Properties: ItemNumber, ItemDescription
- Validation: Record compiles with proper attributes
- [x] 008: Create ProfitCenterFilterEntry record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/FilterEntries/ProfitCenterFilterEntry.cs`
- Properties: Code, Description
- Validation: Record compiles with proper attributes
- [x] 009: Create WorkCenterFilterEntry record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/FilterEntries/WorkCenterFilterEntry.cs`
- Properties: Code, Description
- Validation: Record compiles with proper attributes
- [x] 010: Create OperatorFilterEntry record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/FilterEntries/OperatorFilterEntry.cs`
- Properties: AddressNumber (long), UserID, FullName
- Validation: Record compiles with proper attributes
- [x] 011: Create ComponentLotFilterEntry record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/FilterEntries/ComponentLotFilterEntry.cs`
- Properties: LotNumber, ItemNumber
- Validation: Record compiles with proper attributes
- [x] 012: Create ItemOperationMisFilterEntry record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/FilterEntries/ItemOperationMisFilterEntry.cs`
- Properties: ItemNumber, OperationNumber, MisNumber, MisRevision
- Validation: Record compiles with proper attributes
## Phase 4: Result Records
- [x] 013: Create SearchResult record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/Results/SearchResult.cs`
- Source: `OLD/WorkerService/Models/Reporting/SearchResult.cs`
- Include all OutputColumn attributes matching legacy
- Include computed InclusionReason property
- Validation: All 18 output columns present with correct attributes
- [x] 014: Create MisSearchResult record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/Results/MisSearchResult.cs`
- Source: `OLD/WorkerService/Models/Reporting/MisSearchResult.cs`
- Validation: All MIS-related columns present
- [x] 015: Create MisNonMatchSearchResult record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/Results/MisNonMatchSearchResult.cs`
- Source: `OLD/WorkerService/Models/Reporting/MisNonMatchSearchResult.cs`
- Include: WasJobStepAdded, MatchedJobStepNumber columns
- Validation: All non-match columns present
## Phase 5: SearchModel and Extensions
- [x] 016: Create SearchModel class
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/SearchModel.cs`
- Source: `OLD/WorkerService/Models/Reporting/SearchModel.cs`
- Include all filter collections and computed *Enabled properties
- Include Results, MisResults, MisNonMatchResults collections
- Validation: All filter enabled properties work correctly
- [x] 017: Create SearchQueryResult record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/SearchQueryResult.cs`
- Properties: Sql, Parameters, TempTableSetupSql
- Validation: Record compiles
- [x] 018: Create TableValuedParameterExtensions
- Location: `NEW/src/JdeScoping.SearchProcessing/Extensions/TableValuedParameterExtensions.cs`
- Methods: Create*FilterParameter for all 7 filter types
- Source: `OLD/WorkerService/Helpers/SearchModelHelpers.cs`
- Validation: All TVP methods compile and match legacy schema
- [x] 019: Create SearchModelExtensions
- Location: `NEW/src/JdeScoping.SearchProcessing/Extensions/SearchModelExtensions.cs`
- Methods: ShouldSearchSteps, ToSearchModel (from Search entity)
- Source: `OLD/WorkerService/Helpers/SearchModelHelpers.cs`
- Validation: ShouldSearchSteps logic matches legacy
## Phase 6: Query Builder Interfaces
- [x] 020: Create IFilterHandler interface
- Location: `NEW/src/JdeScoping.SearchProcessing/Interfaces/IFilterHandler.cs`
- Methods: IsEnabled, Apply, Priority property
- Validation: Interface compiles
- [x] 021: Create FilterResult record
- Location: `NEW/src/JdeScoping.SearchProcessing/Models/FilterResult.cs`
- Properties: SetupSql (IReadOnlyList<string>), Parameters (IDictionary)
- Validation: Record compiles
- [x] 022: Create ISearchQueryBuilder interface
- Location: `NEW/src/JdeScoping.SearchProcessing/Interfaces/ISearchQueryBuilder.cs`
- Methods: BuildSearchQuery, BuildMisQuery, BuildMisNonMatchQuery
- Validation: Interface compiles
## Phase 7: Filter Handlers
- [x] 023: Create FilterHandlerBase abstract class
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/FilterHandlerBase.cs`
- Common functionality for all handlers
- Validation: Abstract class compiles
- [x] 024: Create WorkOrderFilterHandler
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/WorkOrderFilterHandler.cs`
- Source: QueryTemplate.tt lines 26-96
- Generates: MERGE #Temp_WO with ManuallySpecified flag, split order detection
- Validation: Unit test verifies SQL structure
- [x] 025: Create ItemNumberFilterHandler
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/ItemNumberFilterHandler.cs`
- Source: QueryTemplate.tt lines 48-64
- Generates: #P_ItemNumbers temp table
- Validation: Unit test verifies SQL structure
- [x] 026: Create ProfitCenterFilterHandler
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/ProfitCenterFilterHandler.cs`
- Source: QueryTemplate.tt lines 65-89
- Generates: #P_WorkCenters temp table via OrgHierarchy join
- Validation: Unit test verifies SQL structure
- [x] 027: Create WorkCenterFilterHandler
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/WorkCenterFilterHandler.cs`
- Source: QueryTemplate.tt lines 90-102
- Generates: MERGE into #P_WorkCenters
- Validation: Unit test verifies SQL structure
- [x] 028: Create OperatorFilterHandler
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/OperatorFilterHandler.cs`
- Source: QueryTemplate.tt lines 147-172
- Generates: #P_OperatorIDs temp table with JdeUser lookup
- Validation: Unit test verifies SQL structure
- [x] 029: Create ComponentLotFilterHandler
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/ComponentLotFilterHandler.cs`
- Source: QueryTemplate.tt lines 103-146
- Generates: WorkOrderComponent/LotUsage joins, CARDEX flag
- Validation: Unit test verifies SQL structure
- [x] 030: Create ItemOperationMisFilterHandler
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/ItemOperationMisFilterHandler.cs`
- Source: QueryTemplate.tt lines 173-198
- Generates: #P_PartOperations temp table
- Validation: Unit test verifies SQL structure
- [x] 031: Create TimespanFilterHandler
- Location: `NEW/src/JdeScoping.SearchProcessing/FilterHandlers/TimespanFilterHandler.cs`
- Source: QueryTemplate.tt lines 254-258
- Generates: WHERE clause with @p_MinimumDT/@p_MaximumDT
- Business rule: Requires BOTH min and max for combined condition
- Validation: Unit test verifies SQL structure
## Phase 8: Query Builder Implementation
- [x] 032: Create SqlKataSearchQueryBuilder
- Location: `NEW/src/JdeScoping.SearchProcessing/QueryBuilders/SqlKataSearchQueryBuilder.cs`
- Orchestrates filter handlers
- Builds #Temp_WO setup, downstream traversal call, final SELECT
- Source: QueryTemplate.tt full template
- Validation: Generated SQL matches legacy structure
- [x] 033: Create MisQueryBuilder
- Location: `NEW/src/JdeScoping.SearchProcessing/QueryBuilders/MisQueryBuilder.cs`
- Builds MIS extraction queries (MIS_CTE, #TempMisData)
- Source: QueryTemplate.tt lines 353-482
- Note: MIS extraction does NOT join #Temp_WO
- Validation: Generated SQL matches legacy structure
## Phase 9: Services
- [x] 034: Create ISearchProcessor interface
- Note: Existing interface at `NEW/src/JdeScoping.Core/Interfaces/ISearchProcessor.cs`
- Methods: ExecuteSearchAsync (IAsyncEnumerable), ExecuteSearchToModelAsync
- Validation: Interface compiles
- [x] 035: Create IWorkOrderTraversalService interface
- Location: `NEW/src/JdeScoping.SearchProcessing/Interfaces/IWorkOrderTraversalService.cs`
- Method: TraverseDownstreamAsync
- Validation: Interface compiles
- [x] 036: Create WorkOrderTraversalService
- Location: `NEW/src/JdeScoping.SearchProcessing/Services/WorkOrderTraversalService.cs`
- Calls dbo.TraverseWorkOrders stored procedure
- Executes WHILE loop logic for downstream work orders
- Source: QueryTemplate.tt lines 285-349
- Validation: Service compiles, stored procedure exists
- [x] 037: Create SearchProcessor service
- Location: `NEW/src/JdeScoping.SearchProcessing/Services/SearchProcessor.cs`
- Constructor: IDbConnectionFactory, ISearchQueryBuilder, IWorkOrderTraversalService, ILotFinderRepository, ILogger
- Implements full search flow:
1. Enrich filter entries via repository lookups
2. Build query via SqlKata
3. Execute temp table setup
4. Call downstream traversal
5. Execute result query
6. Optionally extract MIS data
- Validation: Service compiles, all dependencies injected
## Phase 10: Service Registration
- [x] 038: Create ServiceCollectionExtensions
- Location: `NEW/src/JdeScoping.SearchProcessing/ServiceCollectionExtensions.cs`
- Method: AddSearchProcessing(IServiceCollection, IConfiguration)
- Registers: Options, SqlServerCompiler (singleton), filter handlers (scoped), services (scoped)
- Validation: Services resolve correctly at runtime
- [x] 039: Register search processing in Program.cs
- Location: `NEW/src/JdeScoping.Host/Program.cs`
- Note: Updated to use SearchProcessing module's AddSearchProcessing; renamed Core's method to AddSearchProcessingOptions
- Validation: Application starts without DI errors
## Phase 11: Unit Tests
- [x] 040: Create test project
- Location: `NEW/tests/JdeScoping.SearchProcessing.Tests/JdeScoping.SearchProcessing.Tests.csproj`
- Dependencies: xUnit, Shouldly, NSubstitute
- Validation: Test project builds
- [x] 041: Write WorkOrderFilterHandler tests
- Location: `NEW/tests/JdeScoping.SearchProcessing.Tests/FilterHandlers/WorkOrderFilterHandlerTests.cs`
- Test: IsEnabled returns correct value
- Test: Generated SQL contains MERGE and ManuallySpecified
- Validation: All tests pass (7 tests)
- [x] 042: Write ComponentLotFilterHandler tests
- Location: `NEW/tests/JdeScoping.SearchProcessing.Tests/FilterHandlers/ComponentLotFilterHandlerTests.cs`
- Test: IsEnabled returns correct value
- Test: Generated SQL contains WorkOrderComponent join
- Test: CARDEX flag is set (not PartsList)
- Validation: All tests pass (9 tests)
- [x] 043: Write SqlKataSearchQueryBuilder tests
- Location: `NEW/tests/JdeScoping.SearchProcessing.Tests/QueryBuilders/SqlKataSearchQueryBuilderTests.cs`
- Test: Empty filters produces minimal query
- Test: Single filter produces correct structure
- Test: Multiple filters combine correctly
- Validation: All tests pass (10 tests)
- [x] 044: Write TableValuedParameterExtensions tests
- Location: `NEW/tests/JdeScoping.SearchProcessing.Tests/Extensions/TableValuedParameterExtensionsTests.cs`
- Test: Each Create*Parameter method produces correct DataTable schema
- Test: Empty collections produce empty DataTables
- Validation: All tests pass (17 tests)
- [x] 045: Write SearchResult tests
- Location: `NEW/tests/JdeScoping.SearchProcessing.Tests/Models/SearchResultTests.cs`
- Test: InclusionReason returns correct values for all flag combinations
- Test: ManuallySpecified takes priority over Flagged
- Test: Unknown returned when no flags set
- Validation: All tests pass (21 tests)
## Phase 12: Verification
- [x] 046: Run full test suite
- Command: `dotnet test NEW/tests/JdeScoping.SearchProcessing.Tests/`
- Validation: All 64 tests pass
- [x] 047: Verify solution builds
- Command: `dotnet build NEW/JdeScoping.sln`
- Validation: No errors or warnings
- [x] 048: Run OpenSpec validation
- Command: `openspec validate implement-search-processing --strict`
- Validation: No validation errors - "Change 'implement-search-processing' is valid"
- [x] 049: Codex MCP review of query builder output
- SqlKataSearchQueryBuilder generates SQL matching legacy QueryTemplate.tt structure
- Key features verified: #Temp_WO creation, MERGE statements, filter handler priority ordering
- Validation: SQL structure matches legacy implementation