Files
nt8-sdk/PHASE_A_SPECIFICATION.md
2026-02-24 15:00:41 -05:00

24 KiB

Phase A: NT8 Data & Execution Adapters - Detailed Specification

For: Kilocode AI Agent (Autonomous Implementation)
Phase: Phase A - Foundation
Components: NT8DataAdapter Tests + NT8ExecutionAdapter
Estimated Time: 4-5 hours
Mode: Code Mode


🎯 Objective

Build comprehensive unit tests for existing NT8DataAdapter/NT8DataConverter, then create the NT8ExecutionAdapter that handles real order submission to NinjaTrader 8.


📋 Task 1: NT8 Data Adapter Unit Tests (2 hours)

Overview

The NT8DataConverter.cs already exists but has ZERO unit tests. Create comprehensive test coverage.

Location

Create: tests/NT8.Core.Tests/Adapters/NT8DataConverterTests.cs

Requirements

Test Coverage Target: 95%+ of NT8DataConverter methods

Test Categories:

  1. ConvertBar (8 tests)
  2. ConvertAccount (4 tests)
  3. ConvertPosition (5 tests)
  4. ConvertSession (4 tests)
  5. ConvertContext (6 tests)

Total: 27 unit tests minimum

Detailed Test Specifications

1. ConvertBar Tests (8 tests)

namespace NT8.Core.Tests.Adapters
{
    public class NT8DataConverterTests
    {
        // TEST 1: Happy path with valid ES bar
        [Fact]
        public void ConvertBar_WithValidESBar_ShouldCreateBarData()
        {
            // Input: symbol="ES", time=2026-02-17 09:30:00, OHLCV=4200/4210/4195/4208/10000, barSize=5
            // Expected: BarData with all properties matching, BarSize=TimeSpan.FromMinutes(5)
        }

        // TEST 2: Null/empty/whitespace symbol
        [Theory]
        [InlineData(null)]
        [InlineData("")]
        [InlineData("   ")]
        public void ConvertBar_WithInvalidSymbol_ShouldThrowArgumentException(string symbol)
        {
            // Expected: ArgumentException with parameter name "symbol"
        }

        // TEST 3: Invalid bar sizes (zero, negative)
        [Theory]
        [InlineData(0)]
        [InlineData(-1)]
        [InlineData(-60)]
        public void ConvertBar_WithInvalidBarSize_ShouldThrowArgumentException(int barSize)
        {
            // Expected: ArgumentException with parameter name "barSizeMinutes"
        }

        // TEST 4: Different timeframes (1min, 5min, 15min, 30min, 60min, 240min, daily)
        [Fact]
        public void ConvertBar_WithDifferentTimeframes_ShouldSetCorrectBarSize()
        {
            // Test each: 1, 5, 15, 30, 60, 240, 1440
            // Verify BarSize property matches TimeSpan.FromMinutes(input)
        }

        // TEST 5: High < Low scenario (invalid OHLC)
        [Fact]
        public void ConvertBar_WithHighLessThanLow_ShouldStillCreate()
        {
            // Note: BarData constructor should validate, but converter just passes through
            // Expected: May throw from BarData constructor OR create invalid bar
            // Document actual behavior
        }

        // TEST 6: Zero volume
        [Fact]
        public void ConvertBar_WithZeroVolume_ShouldCreateBar()
        {
            // Expected: Creates bar with Volume=0 (valid for some instruments/sessions)
        }

        // TEST 7: Negative prices
        [Fact]
        public void ConvertBar_WithNegativePrices_ShouldHandleCorrectly()
        {
            // For instruments like ZN that can have negative yields
            // Expected: Accepts negative prices
        }

        // TEST 8: Large volume values
        [Fact]
        public void ConvertBar_WithLargeVolume_ShouldHandleCorrectly()
        {
            // Volume = 10,000,000
            // Expected: Handles long values correctly
        }
    }
}

2. ConvertAccount Tests (4 tests)

// TEST 9: Valid account with positive values
[Fact]
public void ConvertAccount_WithPositiveValues_ShouldCreateAccountInfo()
{
    // Input: equity=100000, buyingPower=250000, dailyPnL=1250.50, maxDD=0.05
    // Expected: All properties match
}

// TEST 10: Negative daily P&L (losing day)
[Fact]
public void ConvertAccount_WithNegativePnL_ShouldHandleCorrectly()
{
    // Input: dailyPnL=-2500.75
    // Expected: DailyPnL property is negative
}

// TEST 11: Zero equity/buying power (margin call scenario)
[Fact]
public void ConvertAccount_WithZeroValues_ShouldCreateAccount()
{
    // Input: All zeros
    // Expected: Creates valid AccountInfo with zero values
}

// TEST 12: Very large equity values
[Fact]
public void ConvertAccount_WithLargeEquity_ShouldHandleCorrectly()
{
    // Input: equity=10,000,000
    // Expected: Handles large doubles correctly
}

3. ConvertPosition Tests (5 tests)

// TEST 13: Long position
[Fact]
public void ConvertPosition_WithLongPosition_ShouldCreatePosition()
{
    // Input: symbol="ES", quantity=2, avgPrice=4200.50, unrealizedPnL=250, realizedPnL=500
    // Expected: Quantity > 0
}

// TEST 14: Short position (negative quantity)
[Fact]
public void ConvertPosition_WithShortPosition_ShouldHandleNegativeQuantity()
{
    // Input: quantity=-1
    // Expected: Quantity < 0
}

// TEST 15: Flat position (zero quantity)
[Fact]
public void ConvertPosition_WithFlatPosition_ShouldHandleZeroQuantity()
{
    // Input: quantity=0, avgPrice=0
    // Expected: Creates valid flat position
}

// TEST 16: Invalid symbol
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("   ")]
public void ConvertPosition_WithInvalidSymbol_ShouldThrowArgumentException(string symbol)
{
    // Expected: ArgumentException with parameter "symbol"
}

// TEST 17: Negative unrealized P&L (losing position)
[Fact]
public void ConvertPosition_WithNegativeUnrealizedPnL_ShouldHandleCorrectly()
{
    // Input: unrealizedPnL=-350.25
    // Expected: UnrealizedPnL property is negative
}

4. ConvertSession Tests (4 tests)

// TEST 18: RTH session
[Fact]
public void ConvertSession_WithRTHSession_ShouldCreateMarketSession()
{
    // Input: start=09:30, end=16:00, isRth=true, name="RTH"
    // Expected: IsRth=true
}

// TEST 19: ETH session
[Fact]
public void ConvertSession_WithETHSession_ShouldCreateMarketSession()
{
    // Input: start=18:00, end=next day 09:30, isRth=false, name="ETH"
    // Expected: IsRth=false, handles overnight session
}

// TEST 20: Invalid session name
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("   ")]
public void ConvertSession_WithInvalidName_ShouldThrowArgumentException(string name)
{
    // Expected: ArgumentException with parameter "sessionName"
}

// TEST 21: End before start (invalid range)
[Fact]
public void ConvertSession_WithEndBeforeStart_ShouldThrowArgumentException()
{
    // Input: start=16:00, end=09:30
    // Expected: ArgumentException with parameter "sessionEnd"
}

5. ConvertContext Tests (6 tests)

// TEST 22: Valid context with all components
[Fact]
public void ConvertContext_WithValidInputs_ShouldCreateStrategyContext()
{
    // Input: All valid Position, Account, Session, CustomData with 2 entries
    // Expected: All properties populated, CustomData contains both entries
}

// TEST 23: Null custom data
[Fact]
public void ConvertContext_WithNullCustomData_ShouldCreateEmptyDictionary()
{
    // Input: customData=null
    // Expected: CustomData is non-null empty dictionary
}

// TEST 24: Invalid symbol
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("   ")]
public void ConvertContext_WithInvalidSymbol_ShouldThrowArgumentException(string symbol)
{
    // Expected: ArgumentException with parameter "symbol"
}

// TEST 25: Null position
[Fact]
public void ConvertContext_WithNullPosition_ShouldThrowArgumentNullException()
{
    // Expected: ArgumentNullException with parameter "currentPosition"
}

// TEST 26: Null account
[Fact]
public void ConvertContext_WithNullAccount_ShouldThrowArgumentNullException()
{
    // Expected: ArgumentNullException with parameter "account"
}

// TEST 27: Null session
[Fact]
public void ConvertContext_WithNullSession_ShouldThrowArgumentNullException()
{
    // Expected: ArgumentNullException with parameter "session"
}

Implementation Notes

Framework:

  • Use xUnit
  • Use FluentAssertions for readable assertions
  • Follow existing test patterns in tests/NT8.Core.Tests

File Structure:

using System;
using System.Collections.Generic;
using Xunit;
using FluentAssertions;
using NT8.Adapters.NinjaTrader;
using NT8.Core.Common.Models;

namespace NT8.Core.Tests.Adapters
{
    /// <summary>
    /// Unit tests for NT8DataConverter
    /// </summary>
    public class NT8DataConverterTests
    {
        // All 27 tests here
    }
}

Success Criteria:

  • All 27 tests implemented
  • All tests pass
  • Zero warnings
  • Code coverage >95% for NT8DataConverter
  • Follows existing test patterns

📋 Task 2: NT8ExecutionAdapter Implementation (2-3 hours)

Overview

Create the adapter that handles REAL order submission to NinjaTrader 8.

Location

Create: src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs
Create: tests/NT8.Core.Tests/Adapters/NT8ExecutionAdapterTests.cs

NT8ExecutionAdapter Specification

Class Structure

using System;
using System.Collections.Generic;
using NT8.Core.Common.Models;
using NT8.Core.OMS;

namespace NT8.Adapters.NinjaTrader
{
    /// <summary>
    /// Adapter for executing orders through NinjaTrader 8 platform.
    /// Bridges SDK order requests to NT8 order submission and handles callbacks.
    /// Thread-safe for concurrent NT8 callbacks.
    /// </summary>
    public class NT8ExecutionAdapter
    {
        private readonly object _lock = new object();
        private readonly Dictionary<string, OrderTrackingInfo> _orderTracking;
        private readonly Dictionary<string, string> _nt8ToSdkOrderMap;
        
        /// <summary>
        /// Creates a new NT8 execution adapter.
        /// </summary>
        public NT8ExecutionAdapter()
        {
            _orderTracking = new Dictionary<string, OrderTrackingInfo>();
            _nt8ToSdkOrderMap = new Dictionary<string, string>();
        }
        
        // Methods defined below...
    }
    
    /// <summary>
    /// Internal class for tracking order state
    /// </summary>
    internal class OrderTrackingInfo
    {
        public string SdkOrderId { get; set; }
        public string Nt8OrderId { get; set; }
        public OrderRequest OriginalRequest { get; set; }
        public OrderState CurrentState { get; set; }
        public int FilledQuantity { get; set; }
        public double AverageFillPrice { get; set; }
        public DateTime LastUpdate { get; set; }
        public string ErrorMessage { get; set; }
    }
}

Method 1: SubmitOrder

/// <summary>
/// Submit an order to NinjaTrader 8.
/// NOTE: This method accepts primitive parameters instead of NT8 Strategy object
/// to maintain testability and avoid NT8 DLL dependencies in core adapter.
/// The actual NT8StrategyBase will call NT8 methods and pass results here.
/// </summary>
/// <param name="request">SDK order request</param>
/// <param name="sdkOrderId">Unique SDK order ID</param>
/// <returns>Tracking info for the submitted order</returns>
/// <exception cref="ArgumentNullException">If request or orderId is null</exception>
/// <exception cref="InvalidOperationException">If order already exists</exception>
public OrderTrackingInfo SubmitOrder(OrderRequest request, string sdkOrderId)
{
    if (request == null)
        throw new ArgumentNullException("request");
    if (string.IsNullOrWhiteSpace(sdkOrderId))
        throw new ArgumentNullException("sdkOrderId");
    
    lock (_lock)
    {
        // Check if order already tracked
        if (_orderTracking.ContainsKey(sdkOrderId))
        {
            throw new InvalidOperationException(
                string.Format("Order {0} already exists", sdkOrderId));
        }
        
        // Create tracking info
        var trackingInfo = new OrderTrackingInfo
        {
            SdkOrderId = sdkOrderId,
            Nt8OrderId = null, // Will be set by NT8 callback
            OriginalRequest = request,
            CurrentState = OrderState.Pending,
            FilledQuantity = 0,
            AverageFillPrice = 0.0,
            LastUpdate = DateTime.UtcNow,
            ErrorMessage = null
        };
        
        _orderTracking[sdkOrderId] = trackingInfo;
        
        // NOTE: Actual NT8 submission happens in NT8StrategyBase
        // This adapter only tracks state
        
        return trackingInfo;
    }
}

Method 2: ProcessOrderUpdate

/// <summary>
/// Process order update callback from NinjaTrader 8.
/// Called by NT8StrategyBase.OnOrderUpdate().
/// </summary>
/// <param name="nt8OrderId">NT8's order ID</param>
/// <param name="sdkOrderId">SDK's order ID (from order name/tag)</param>
/// <param name="orderState">NT8 order state</param>
/// <param name="filled">Filled quantity</param>
/// <param name="averageFillPrice">Average fill price</param>
/// <param name="errorCode">Error code if rejected</param>
/// <param name="errorMessage">Error message if rejected</param>
public void ProcessOrderUpdate(
    string nt8OrderId,
    string sdkOrderId,
    string orderState,
    int filled,
    double averageFillPrice,
    int errorCode,
    string errorMessage)
{
    if (string.IsNullOrWhiteSpace(sdkOrderId))
        return; // Ignore orders not from SDK
    
    lock (_lock)
    {
        if (!_orderTracking.ContainsKey(sdkOrderId))
        {
            // Order not tracked, ignore
            return;
        }
        
        var info = _orderTracking[sdkOrderId];
        
        // Map NT8 order ID
        if (!string.IsNullOrWhiteSpace(nt8OrderId) && info.Nt8OrderId == null)
        {
            info.Nt8OrderId = nt8OrderId;
            _nt8ToSdkOrderMap[nt8OrderId] = sdkOrderId;
        }
        
        // Update state
        info.CurrentState = MapNT8OrderState(orderState);
        info.FilledQuantity = filled;
        info.AverageFillPrice = averageFillPrice;
        info.LastUpdate = DateTime.UtcNow;
        
        // Handle errors
        if (errorCode != 0 && !string.IsNullOrWhiteSpace(errorMessage))
        {
            info.ErrorMessage = string.Format("[{0}] {1}", errorCode, errorMessage);
            info.CurrentState = OrderState.Rejected;
        }
    }
}

Method 3: ProcessExecution

/// <summary>
/// Process execution (fill) callback from NinjaTrader 8.
/// Called by NT8StrategyBase.OnExecutionUpdate().
/// </summary>
/// <param name="nt8OrderId">NT8 order ID</param>
/// <param name="executionId">NT8 execution ID</param>
/// <param name="price">Fill price</param>
/// <param name="quantity">Fill quantity</param>
/// <param name="time">Execution time</param>
public void ProcessExecution(
    string nt8OrderId,
    string executionId,
    double price,
    int quantity,
    DateTime time)
{
    if (string.IsNullOrWhiteSpace(nt8OrderId))
        return;
    
    lock (_lock)
    {
        // Map NT8 order ID to SDK order ID
        if (!_nt8ToSdkOrderMap.ContainsKey(nt8OrderId))
            return; // Not our order
        
        var sdkOrderId = _nt8ToSdkOrderMap[nt8OrderId];
        if (!_orderTracking.ContainsKey(sdkOrderId))
            return;
        
        var info = _orderTracking[sdkOrderId];
        
        // Update fill info
        // Note: NT8 may send multiple execution callbacks for partial fills
        // We track cumulative filled quantity via ProcessOrderUpdate
        
        info.LastUpdate = time;
        
        // Update state based on filled quantity
        if (info.FilledQuantity >= info.OriginalRequest.Quantity)
        {
            info.CurrentState = OrderState.Filled;
        }
        else if (info.FilledQuantity > 0)
        {
            info.CurrentState = OrderState.PartiallyFilled;
        }
    }
}

Method 4: CancelOrder

/// <summary>
/// Request to cancel an order.
/// NOTE: Actual cancellation happens in NT8StrategyBase via CancelOrder().
/// This method validates and marks order for cancellation.
/// </summary>
/// <param name="sdkOrderId">SDK order ID to cancel</param>
/// <returns>True if cancel request accepted, false if order can't be cancelled</returns>
public bool CancelOrder(string sdkOrderId)
{
    if (string.IsNullOrWhiteSpace(sdkOrderId))
        throw new ArgumentNullException("sdkOrderId");
    
    lock (_lock)
    {
        if (!_orderTracking.ContainsKey(sdkOrderId))
            return false; // Order not found
        
        var info = _orderTracking[sdkOrderId];
        
        // Check if order is in cancellable state
        if (info.CurrentState == OrderState.Filled ||
            info.CurrentState == OrderState.Cancelled ||
            info.CurrentState == OrderState.Rejected)
        {
            return false; // Already in terminal state
        }
        
        // Mark as pending cancellation
        // Actual state change happens in ProcessOrderUpdate callback
        info.LastUpdate = DateTime.UtcNow;
        
        return true;
    }
}

Method 5: GetOrderStatus

/// <summary>
/// Get current status of an order.
/// </summary>
/// <param name="sdkOrderId">SDK order ID</param>
/// <returns>Order status or null if not found</returns>
public OrderStatus GetOrderStatus(string sdkOrderId)
{
    if (string.IsNullOrWhiteSpace(sdkOrderId))
        return null;
    
    lock (_lock)
    {
        if (!_orderTracking.ContainsKey(sdkOrderId))
            return null;
        
        var info = _orderTracking[sdkOrderId];
        
        return new OrderStatus(
            orderId: info.SdkOrderId,
            state: info.CurrentState,
            symbol: info.OriginalRequest.Symbol,
            side: info.OriginalRequest.Side,
            quantity: info.OriginalRequest.Quantity,
            type: info.OriginalRequest.Type,
            filled: info.FilledQuantity,
            averageFillPrice: info.FilledQuantity > 0 ? (double?)info.AverageFillPrice : null,
            message: info.ErrorMessage,
            timestamp: info.LastUpdate
        );
    }
}

Helper Method: MapNT8OrderState

/// <summary>
/// Maps NinjaTrader 8 order state strings to SDK OrderState enum.
/// </summary>
/// <param name="nt8State">NT8 order state as string</param>
/// <returns>Mapped SDK OrderState</returns>
private OrderState MapNT8OrderState(string nt8State)
{
    if (string.IsNullOrWhiteSpace(nt8State))
        return OrderState.Unknown;
    
    // NT8 order states: https://ninjatrader.com/support/helpGuides/nt8/?orderstate.htm
    switch (nt8State.ToUpperInvariant())
    {
        case "ACCEPTED":
        case "WORKING":
            return OrderState.Working;
        
        case "FILLED":
            return OrderState.Filled;
        
        case "PARTFILLED":
        case "PARTIALLYFILLED":
            return OrderState.PartiallyFilled;
        
        case "CANCELLED":
        case "CANCELED":
            return OrderState.Cancelled;
        
        case "REJECTED":
            return OrderState.Rejected;
        
        case "PENDINGCANCEL":
            return OrderState.Working; // Still working until cancelled
        
        case "PENDINGCHANGE":
        case "PENDINGSUBMIT":
            return OrderState.Pending;
        
        default:
            return OrderState.Unknown;
    }
}

Unit Tests for NT8ExecutionAdapter

Create: tests/NT8.Core.Tests/Adapters/NT8ExecutionAdapterTests.cs

Test Count: 15 tests minimum

public class NT8ExecutionAdapterTests
{
    // TEST 1: Submit valid order
    [Fact]
    public void SubmitOrder_WithValidRequest_ShouldCreateTrackingInfo()
    
    // TEST 2: Submit duplicate order
    [Fact]
    public void SubmitOrder_WithDuplicateOrderId_ShouldThrowInvalidOperationException()
    
    // TEST 3: Submit with null request
    [Fact]
    public void SubmitOrder_WithNullRequest_ShouldThrowArgumentNullException()
    
    // TEST 4: Process order update - Working state
    [Fact]
    public void ProcessOrderUpdate_WithWorkingState_ShouldUpdateState()
    
    // TEST 5: Process order update - Filled state
    [Fact]
    public void ProcessOrderUpdate_WithFilledState_ShouldMarkFilled()
    
    // TEST 6: Process order update - Rejected with error
    [Fact]
    public void ProcessOrderUpdate_WithRejection_ShouldSetErrorMessage()
    
    // TEST 7: Process execution - Full fill
    [Fact]
    public void ProcessExecution_WithFullFill_ShouldMarkFilled()
    
    // TEST 8: Process execution - Partial fill
    [Fact]
    public void ProcessExecution_WithPartialFill_ShouldMarkPartiallyFilled()
    
    // TEST 9: Cancel order - Valid
    [Fact]
    public void CancelOrder_WithWorkingOrder_ShouldReturnTrue()
    
    // TEST 10: Cancel order - Already filled
    [Fact]
    public void CancelOrder_WithFilledOrder_ShouldReturnFalse()
    
    // TEST 11: Get order status - Exists
    [Fact]
    public void GetOrderStatus_WithExistingOrder_ShouldReturnStatus()
    
    // TEST 12: Get order status - Not found
    [Fact]
    public void GetOrderStatus_WithNonExistentOrder_ShouldReturnNull()
    
    // TEST 13: NT8 order state mapping
    [Theory]
    [InlineData("ACCEPTED", OrderState.Working)]
    [InlineData("FILLED", OrderState.Filled)]
    [InlineData("CANCELLED", OrderState.Cancelled)]
    [InlineData("REJECTED", OrderState.Rejected)]
    public void MapNT8OrderState_WithKnownStates_ShouldMapCorrectly(string nt8State, OrderState expected)
    
    // TEST 14: Thread safety - Concurrent submissions
    [Fact]
    public void SubmitOrder_WithConcurrentCalls_ShouldBeThreadSafe()
    
    // TEST 15: Multiple executions for same order
    [Fact]
    public void ProcessExecution_WithMultipleCallsForSameOrder_ShouldAccumulate()
}

Success Criteria

For NT8ExecutionAdapter:

  • All public methods implemented
  • Thread-safe with lock protection
  • Comprehensive XML documentation
  • C# 5.0 compliant (no modern syntax)
  • Zero build warnings

For Tests:

  • All 15 tests implemented
  • All tests pass
  • Code coverage >90% for NT8ExecutionAdapter
  • Thread safety validated

🔄 Implementation Workflow

Step 1: Create Test File (30 min)

  1. Create tests/NT8.Core.Tests/Adapters/ directory
  2. Create NT8DataConverterTests.cs
  3. Implement all 27 tests
  4. Run tests - should all PASS (code already exists)

Step 2: Verify Test Coverage (15 min)

dotnet test --collect:"XPlat Code Coverage"
# Verify >95% coverage for NT8DataConverter

Step 3: Create NT8ExecutionAdapter (2 hours)

  1. Create src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs
  2. Implement all methods per specification
  3. Add XML documentation
  4. Verify C# 5.0 compliance

Step 4: Create Execution Adapter Tests (1 hour)

  1. Create tests/NT8.Core.Tests/Adapters/NT8ExecutionAdapterTests.cs
  2. Implement all 15 tests
  3. Run tests - should all PASS

Step 5: Build & Verify (15 min)

dotnet build --configuration Release
dotnet test --configuration Release
.\verify-build.bat

Step 6: Git Commit

git add tests/NT8.Core.Tests/Adapters/
git add src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs
git commit -m "feat: Add NT8 adapter tests and execution adapter

- Added 27 unit tests for NT8DataConverter (>95% coverage)
- Implemented NT8ExecutionAdapter with order tracking
- Added 15 unit tests for NT8ExecutionAdapter (>90% coverage)
- Thread-safe order state management
- NT8 order state mapping
- C# 5.0 compliant

Phase A complete: Foundation adapters ready for NT8 integration"

📊 Deliverables Checklist

  • tests/NT8.Core.Tests/Adapters/NT8DataConverterTests.cs (27 tests)
  • src/NT8.Adapters/NinjaTrader/NT8ExecutionAdapter.cs (full implementation)
  • tests/NT8.Core.Tests/Adapters/NT8ExecutionAdapterTests.cs (15 tests)
  • All 42 new tests passing
  • All 240+ existing tests still passing
  • Zero build warnings
  • Code coverage: >95% for DataConverter, >90% for ExecutionAdapter
  • Git commit with clear message

🚨 Important Constraints

  1. C# 5.0 Only - No:

    • async/await
    • String interpolation $""
    • Expression-bodied members =>
    • Pattern matching
    • Tuples
    • Use string.Format() instead of $""
  2. Thread Safety - All shared state must use lock (_lock)

  3. Defensive Programming - Validate all inputs, null checks

  4. XML Documentation - All public members must have /// comments

  5. Test Patterns - Follow existing test conventions in tests/NT8.Core.Tests


🎯 Success Metrics

Definition of Done:

  • All 42 tests passing
  • All existing 240+ tests still passing
  • Build succeeds with zero warnings
  • Code coverage targets met
  • Thread safety verified
  • C# 5.0 compliant
  • Committed to Git

Time Target: 4-5 hours total


READY FOR KILOCODE EXECUTION IN CODE MODE