diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 5e2afec..8f1f9e5 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,7 +1,14 @@
{
"permissions": {
"allow": [
- "Bash(powershell -Command \"Get-Content ''d:\\\\Development\\\\Tracking_Kits\\\\ImportService\\\\TrackingDataImport.vbs'' -Encoding Unicode | Out-String\")"
+ "Bash(powershell -Command \"Get-Content ''d:\\\\Development\\\\Tracking_Kits\\\\ImportService\\\\TrackingDataImport.vbs'' -Encoding Unicode | Out-String\")",
+ "Bash(git -C \"d:/Development/Tracking_Kits\" rev-parse HEAD)",
+ "Bash(cscript //NoLogo Tests/TrackingDataImport_TestHarness.vbs)",
+ "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix_harness.ps1\")",
+ "Bash(powershell -Command \":*)",
+ "Bash(del \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix_harness.ps1\")",
+ "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix2.ps1\")",
+ "Bash(powershell -ExecutionPolicy Bypass -File \"d:\\\\Development\\\\Tracking_Kits\\\\Tests\\\\_fix3.ps1\")"
]
}
}
diff --git a/App/Controllers/Kit/KitController.asp b/App/Controllers/Kit/KitController.asp
index a76476e..8e2d13d 100644
--- a/App/Controllers/Kit/KitController.asp
+++ b/App/Controllers/Kit/KitController.asp
@@ -1,6 +1,7 @@
<% Option Explicit %>
+
<%
Class KitController
Public Model
@@ -40,9 +41,7 @@ Class KitController
dim ID : ID = Request.Form("Id")
dim model : set model = KitRepository.FindByID(ID)
set model = Automapper.AutoMap(Request.Form, model)
- if Request.Form("InBoundTracking") = "on" Then
- model.InboundSTID = SettingsRepository.FindByName("Inbound STID")
- end if
+ model.InboundSTID = Null
model.Status = "Ready to Assign Labels"
@@ -99,10 +98,10 @@ Class KitController
set Model.Kit = KitRepository.SwitchBoardPurpleEnvelopeEditFindById(id)
set Model.StidDropDown = SettingsRepository.GetStidDropDownRS()
set Model.ColorsDropDown = ColorsRepository.GetColorsDropDownRS()
- set Model.Precincts = SortPrecinctColorRows(InkjetRecordsRepository.GetDistinctPrecinctsByKitId(id))
- set Model.PrecinctBallotRanges = SortPrecinctBallotRangeRows(InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(id))
+ set Model.Precincts = PurpleEnvelopeReportHelper().SortPrecinctColorRows(InkjetRecordsRepository.GetDistinctPrecinctsByKitId(id))
+ set Model.PrecinctBallotRanges = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(id))
On Error Resume Next
- Model.PurpleEnvelopeElectionLabel = FormatPurpleEnvelopeElectionLabel(SettingsRepository.FindByName("Electiondate"))
+ Model.PurpleEnvelopeElectionLabel = PurpleEnvelopeReportHelper().FormatElectionLabel(SettingsRepository.FindByName("Electiondate"))
If Err.Number <> 0 Then
Model.PurpleEnvelopeElectionLabel = ""
Err.Clear
@@ -114,158 +113,6 @@ Class KitController
%> <%
End Sub
- Private Function FormatPurpleEnvelopeElectionLabel(ByVal rawValue)
- FormatPurpleEnvelopeElectionLabel = Trim(rawValue & "")
- If Len(FormatPurpleEnvelopeElectionLabel) = 0 Then Exit Function
-
- On Error Resume Next
- dim parsedDate : parsedDate = CDate(rawValue)
- If Err.Number = 0 Then
- FormatPurpleEnvelopeElectionLabel = MonthName(Month(parsedDate), True) & "-" & CStr(Year(parsedDate))
- Else
- Err.Clear
- End If
- On Error GoTo 0
- End Function
-
- Private Function SortPrecinctColorRows(ByVal rs)
- dim list : set list = new LinkedList_Class
- If rs.EOF Then
- set SortPrecinctColorRows = list
- Exit Function
- End If
-
- dim items()
- dim itemCount : itemCount = -1
- Do Until rs.EOF
- itemCount = itemCount + 1
- ReDim Preserve items(itemCount)
-
- dim row : set row = new PrecinctColorRow_ViewModel_Class
- row.PRECINCT = rs("PRECINCT")
- If IsNull(rs("ColorId")) Then
- row.ColorId = ""
- Else
- row.ColorId = rs("ColorId")
- End If
- Set items(itemCount) = row
- rs.MoveNext
- Loop
-
- SortPrecinctItems items
-
- dim i
- For i = 0 To UBound(items)
- list.Push items(i)
- Next
-
- set SortPrecinctColorRows = list
- End Function
-
- Private Function SortPrecinctBallotRangeRows(ByVal rs)
- dim list : set list = new LinkedList_Class
- If rs.EOF Then
- set SortPrecinctBallotRangeRows = list
- Exit Function
- End If
-
- dim items()
- dim itemCount : itemCount = -1
- Do Until rs.EOF
- itemCount = itemCount + 1
- ReDim Preserve items(itemCount)
-
- dim row : set row = new PrecinctBallotRangeRow_ViewModel_Class
- row.PRECINCT = rs("PRECINCT")
- row.LowBallotNumber = rs("LowBallotNumber")
- row.HighBallotNumber = rs("HighBallotNumber")
- Set items(itemCount) = row
- rs.MoveNext
- Loop
-
- SortPrecinctItems items
-
- dim i
- For i = 0 To UBound(items)
- list.Push items(i)
- Next
-
- set SortPrecinctBallotRangeRows = list
- End Function
-
- Private Sub SortPrecinctItems(ByRef items)
- If Not IsArray(items) Then Exit Sub
-
- dim i, j
- For i = 0 To UBound(items) - 1
- For j = i + 1 To UBound(items)
- If PrecinctSortsBefore(items(j).PRECINCT, items(i).PRECINCT) Then
- dim temp : set temp = items(i)
- Set items(i) = items(j)
- Set items(j) = temp
- End If
- Next
- Next
- End Sub
-
- Private Function PrecinctSortsBefore(ByVal leftPrecinct, ByVal rightPrecinct)
- dim leftType, leftNumber, leftSuffix, leftNormalized
- dim rightType, rightNumber, rightSuffix, rightNormalized
-
- ParsePrecinctSortParts leftPrecinct, leftType, leftNumber, leftSuffix, leftNormalized
- ParsePrecinctSortParts rightPrecinct, rightType, rightNumber, rightSuffix, rightNormalized
-
- If leftType <> rightType Then
- PrecinctSortsBefore = (leftType < rightType)
- Exit Function
- End If
-
- If leftType = 0 Then
- If leftNumber <> rightNumber Then
- PrecinctSortsBefore = (leftNumber < rightNumber)
- Exit Function
- End If
-
- If leftSuffix <> rightSuffix Then
- PrecinctSortsBefore = (leftSuffix < rightSuffix)
- Exit Function
- End If
- End If
-
- If leftNormalized <> rightNormalized Then
- PrecinctSortsBefore = (leftNormalized < rightNormalized)
- Else
- PrecinctSortsBefore = (UCase(Trim(leftPrecinct & "")) < UCase(Trim(rightPrecinct & "")))
- End If
- End Function
-
- Private Sub ParsePrecinctSortParts(ByVal precinct, ByRef precinctType, ByRef numericPart, ByRef suffixPart, ByRef normalizedText)
- dim rawPrecinct : rawPrecinct = Trim(precinct & "")
- dim leadingDigits : leadingDigits = ""
- dim i, currentChar
-
- For i = 1 To Len(rawPrecinct)
- currentChar = Mid(rawPrecinct, i, 1)
- If currentChar >= "0" And currentChar <= "9" Then
- leadingDigits = leadingDigits & currentChar
- Else
- Exit For
- End If
- Next
-
- If Len(leadingDigits) > 0 Then
- precinctType = 0
- numericPart = CLng(leadingDigits)
- suffixPart = UCase(Trim(Mid(rawPrecinct, Len(leadingDigits) + 1)))
- normalizedText = CStr(numericPart) & "|" & suffixPart
- Else
- precinctType = 1
- numericPart = 0
- suffixPart = UCase(rawPrecinct)
- normalizedText = suffixPart
- End If
- End Sub
-
Public Sub Index
dim page_size : page_size = 10
diff --git a/App/DomainModels/PurpleEnvelopeReportHelper.asp b/App/DomainModels/PurpleEnvelopeReportHelper.asp
new file mode 100644
index 0000000..4dfaa79
--- /dev/null
+++ b/App/DomainModels/PurpleEnvelopeReportHelper.asp
@@ -0,0 +1,164 @@
+<%
+Class PurpleEnvelopeReportHelper_Class
+
+ Public Function FormatElectionLabel(ByVal rawValue)
+ FormatElectionLabel = Trim(rawValue & "")
+ If Len(FormatElectionLabel) = 0 Then Exit Function
+
+ On Error Resume Next
+ dim parsedDate : parsedDate = CDate(rawValue)
+ If Err.Number = 0 Then
+ FormatElectionLabel = MonthName(Month(parsedDate), True) & "-" & CStr(Year(parsedDate))
+ Else
+ Err.Clear
+ End If
+ On Error GoTo 0
+ End Function
+
+ Public Function SortPrecinctColorRows(ByVal rs)
+ dim list : set list = new LinkedList_Class
+ If rs.EOF Then
+ set SortPrecinctColorRows = list
+ Exit Function
+ End If
+
+ dim items()
+ dim itemCount : itemCount = -1
+ Do Until rs.EOF
+ itemCount = itemCount + 1
+ ReDim Preserve items(itemCount)
+
+ dim row : set row = new PrecinctColorRow_ViewModel_Class
+ row.PRECINCT = rs("PRECINCT")
+ If IsNull(rs("ColorId")) Then
+ row.ColorId = ""
+ Else
+ row.ColorId = rs("ColorId")
+ End If
+ Set items(itemCount) = row
+ rs.MoveNext
+ Loop
+
+ SortPrecinctItems items
+
+ dim i
+ For i = 0 To UBound(items)
+ list.Push items(i)
+ Next
+
+ set SortPrecinctColorRows = list
+ End Function
+
+ Public Function SortPrecinctBallotRangeRows(ByVal rs)
+ dim list : set list = new LinkedList_Class
+ If rs.EOF Then
+ set SortPrecinctBallotRangeRows = list
+ Exit Function
+ End If
+
+ dim items()
+ dim itemCount : itemCount = -1
+ Do Until rs.EOF
+ itemCount = itemCount + 1
+ ReDim Preserve items(itemCount)
+
+ dim row : set row = new PrecinctBallotRangeRow_ViewModel_Class
+ row.PRECINCT = rs("PRECINCT")
+ row.LowBallotNumber = rs("LowBallotNumber")
+ row.HighBallotNumber = rs("HighBallotNumber")
+ Set items(itemCount) = row
+ rs.MoveNext
+ Loop
+
+ SortPrecinctItems items
+
+ dim i
+ For i = 0 To UBound(items)
+ list.Push items(i)
+ Next
+
+ set SortPrecinctBallotRangeRows = list
+ End Function
+
+ Private Sub SortPrecinctItems(ByRef items)
+ If Not IsArray(items) Then Exit Sub
+
+ dim i, j
+ For i = 0 To UBound(items) - 1
+ For j = i + 1 To UBound(items)
+ If PrecinctSortsBefore(items(j).PRECINCT, items(i).PRECINCT) Then
+ dim temp : set temp = items(i)
+ Set items(i) = items(j)
+ Set items(j) = temp
+ End If
+ Next
+ Next
+ End Sub
+
+ Private Function PrecinctSortsBefore(ByVal leftPrecinct, ByVal rightPrecinct)
+ dim leftType, leftNumber, leftSuffix, leftNormalized
+ dim rightType, rightNumber, rightSuffix, rightNormalized
+
+ ParsePrecinctSortParts leftPrecinct, leftType, leftNumber, leftSuffix, leftNormalized
+ ParsePrecinctSortParts rightPrecinct, rightType, rightNumber, rightSuffix, rightNormalized
+
+ If leftType <> rightType Then
+ PrecinctSortsBefore = (leftType < rightType)
+ Exit Function
+ End If
+
+ If leftType = 0 Then
+ If leftNumber <> rightNumber Then
+ PrecinctSortsBefore = (leftNumber < rightNumber)
+ Exit Function
+ End If
+
+ If leftSuffix <> rightSuffix Then
+ PrecinctSortsBefore = (leftSuffix < rightSuffix)
+ Exit Function
+ End If
+ End If
+
+ If leftNormalized <> rightNormalized Then
+ PrecinctSortsBefore = (leftNormalized < rightNormalized)
+ Else
+ PrecinctSortsBefore = (UCase(Trim(leftPrecinct & "")) < UCase(Trim(rightPrecinct & "")))
+ End If
+ End Function
+
+ Private Sub ParsePrecinctSortParts(ByVal precinct, ByRef precinctType, ByRef numericPart, ByRef suffixPart, ByRef normalizedText)
+ dim rawPrecinct : rawPrecinct = Trim(precinct & "")
+ dim leadingDigits : leadingDigits = ""
+ dim i, currentChar
+
+ For i = 1 To Len(rawPrecinct)
+ currentChar = Mid(rawPrecinct, i, 1)
+ If currentChar >= "0" And currentChar <= "9" Then
+ leadingDigits = leadingDigits & currentChar
+ Else
+ Exit For
+ End If
+ Next
+
+ If Len(leadingDigits) > 0 Then
+ precinctType = 0
+ numericPart = CLng(leadingDigits)
+ suffixPart = UCase(Trim(Mid(rawPrecinct, Len(leadingDigits) + 1)))
+ normalizedText = CStr(numericPart) & "|" & suffixPart
+ Else
+ precinctType = 1
+ numericPart = 0
+ suffixPart = UCase(rawPrecinct)
+ normalizedText = suffixPart
+ End If
+ End Sub
+End Class
+
+dim PurpleEnvelopeReportHelper__Singleton
+Function PurpleEnvelopeReportHelper()
+ If IsEmpty(PurpleEnvelopeReportHelper__Singleton) Then
+ set PurpleEnvelopeReportHelper__Singleton = new PurpleEnvelopeReportHelper_Class
+ End If
+ set PurpleEnvelopeReportHelper = PurpleEnvelopeReportHelper__Singleton
+End Function
+%>
diff --git a/App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp b/App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp
index 6b47c56..3c60c34 100644
--- a/App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp
+++ b/App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp
@@ -105,8 +105,6 @@
<%= HTML.Hidden("Id", Model.Kit.ID) %>
<%= HTML.DropDownListExt("OutboundSTID","hmm",Model.StidDropDown,"STID","OPTION",Array("Class","form-select")) %>
- <%= HTML.Checkbox("InBoundTracking",0) %>Inbound Tracking
-
<%= HTML.Button("submit", " Save", "btn-primary") %>
diff --git a/App/app.config.asp b/App/app.config.asp
index 793dff3..c7c877a 100644
--- a/App/app.config.asp
+++ b/App/app.config.asp
@@ -18,12 +18,44 @@ select case dev
ExportDirectory ="\\kci-syn-cl01\PC Transfer\TrackingDataExport\"
end select
+
+Function LoadChilkatSerial()
+ dim fallbackSerial : fallbackSerial = "KENTCM.CB1022025_RGzBPM5J655e"
+ LoadChilkatSerial = fallbackSerial
+
+ On Error Resume Next
+ dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject")
+ dim serialPath : serialPath = Request.ServerVariables("APPL_PHYSICAL_PATH") & "Tests\chillkat_serial"
+
+ If Err.Number = 0 Then
+ If fso.FileExists(serialPath) Then
+ dim serialFile : set serialFile = fso.OpenTextFile(serialPath, 1, False)
+ If Err.Number = 0 Then
+ dim fileSerial : fileSerial = Trim(serialFile.ReadAll())
+ serialFile.Close
+ set serialFile = Nothing
+
+ If Len(fileSerial) > 0 Then
+ LoadChilkatSerial = fileSerial
+ End If
+ Else
+ Err.Clear
+ End If
+ End If
+ Else
+ Err.Clear
+ End If
+
+ set fso = Nothing
+ On Error GoTo 0
+End Function
+
'=======================================================================================================================
' Set Global Variables Here
'=======================================================================================================================
dim glob:set glob = Server.CreateObject("Chilkat_9_5_0.Global")
-dim success:success = glob.UnlockBundle("KENTCM.CB1022025_RGzBPM5J655e")
+dim success:success = glob.UnlockBundle(LoadChilkatSerial())
If (success <> 1) Then
put(glob.LastErrorText)
End If
-%>
\ No newline at end of file
+%>
diff --git a/ImportService/TrackingDataImport.vbs b/ImportService/TrackingDataImport.vbs
index d16e8f7..7299180 100644
Binary files a/ImportService/TrackingDataImport.vbs and b/ImportService/TrackingDataImport.vbs differ
diff --git a/Tests/TestCase_PurpleEnvelopeReport.asp b/Tests/TestCase_PurpleEnvelopeReport.asp
new file mode 100644
index 0000000..9512d0a
--- /dev/null
+++ b/Tests/TestCase_PurpleEnvelopeReport.asp
@@ -0,0 +1,386 @@
+
+
+
+
+
+
+<%
+Class PurpleEnvelopeReport_Tests
+ Private m_tempDbPath
+
+ Public Sub Setup
+ m_tempDbPath = CreateDisposableDatabaseCopy()
+ UseDisposableDatabase m_tempDbPath
+ End Sub
+
+ Public Sub Teardown
+ ResetDisposableDatabase
+ End Sub
+
+ Public Function TestCaseNames
+ TestCaseNames = Array( _
+ "Test_FormatElectionLabel_Returns_Mon_YYYY_For_Valid_Date", _
+ "Test_FormatElectionLabel_Returns_Trimmed_Raw_Value_For_Invalid_Date", _
+ "Test_SortPrecinctColorRows_Handles_Zero_Padded_And_Lettered_Precincts", _
+ "Test_SortPrecinctBallotRangeRows_Preserves_Ranges_While_Sorting", _
+ "Test_GetPrecinctBallotRangesByKitId_Uses_Seeded_Data", _
+ "Test_UpdateColorForKit_Updates_All_Seeded_Rows_For_The_Target_Kit", _
+ "Test_UpdateColorForPrecinct_Updates_Only_The_Targeted_Precinct", _
+ "Test_SwitchBoardPurpleEnvelopeEditFindById_Returns_Seeded_Header_Data", _
+ "Test_KitController_Post_Actions_Still_Delegate_To_Color_Update_Repositories", _
+ "Test_Report_View_Keeps_Print_Only_CSS_Contract", _
+ "Test_Report_View_Keeps_No_Data_Row_And_Page_Spacer" _
+ )
+ End Function
+
+ Private Sub Destroy(ByRef o)
+ On Error Resume Next
+ o.Close
+ On Error GoTo 0
+ Set o = Nothing
+ End Sub
+
+ Private Function CreatePrecinctColorRecordset(ByVal rows)
+ dim rs : set rs = Server.CreateObject("ADODB.Recordset")
+ With rs.Fields
+ .Append "PRECINCT", adVarChar, 50
+ .Append "ColorId", adVarChar, 50
+ End With
+
+ rs.Open
+
+ dim i
+ For i = 0 To UBound(rows)
+ rs.AddNew
+ rs("PRECINCT") = rows(i)(0)
+ If IsNull(rows(i)(1)) Then
+ rs("ColorId") = Null
+ Else
+ rs("ColorId") = rows(i)(1)
+ End If
+ rs.Update
+ Next
+
+ rs.MoveFirst
+ set CreatePrecinctColorRecordset = rs
+ End Function
+
+ Private Function CreateBallotRangeRecordset(ByVal rows)
+ dim rs : set rs = Server.CreateObject("ADODB.Recordset")
+ With rs.Fields
+ .Append "PRECINCT", adVarChar, 50
+ .Append "LowBallotNumber", adInteger
+ .Append "HighBallotNumber", adInteger
+ End With
+
+ rs.Open
+
+ dim i
+ For i = 0 To UBound(rows)
+ rs.AddNew
+ rs("PRECINCT") = rows(i)(0)
+ rs("LowBallotNumber") = rows(i)(1)
+ rs("HighBallotNumber") = rows(i)(2)
+ rs.Update
+ Next
+
+ rs.MoveFirst
+ set CreateBallotRangeRecordset = rs
+ End Function
+
+ Private Function PrecinctListToCsv(ByVal list)
+ dim values()
+ dim idx : idx = -1
+ dim it : set it = list.Iterator()
+ dim row
+
+ Do While it.HasNext
+ set row = it.GetNext()
+ idx = idx + 1
+ ReDim Preserve values(idx)
+ values(idx) = row.PRECINCT
+ Loop
+
+ If idx = -1 Then
+ PrecinctListToCsv = ""
+ Else
+ PrecinctListToCsv = Join(values, ",")
+ End If
+ End Function
+
+ Private Function ReadAllText(ByVal relativePath)
+ dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject")
+ dim fileHandle : set fileHandle = fso.OpenTextFile(Server.MapPath(relativePath), 1, False)
+ ReadAllText = fileHandle.ReadAll()
+ fileHandle.Close
+ Set fileHandle = Nothing
+ Set fso = Nothing
+ End Function
+
+ Private Function CreateDisposableDatabaseCopy()
+ dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject")
+ dim tempFolderPath : tempFolderPath = Server.MapPath("Temp")
+ If Not fso.FolderExists(tempFolderPath) Then
+ fso.CreateFolder tempFolderPath
+ End If
+
+ dim sourcePath : sourcePath = Server.MapPath("../Data/webdata - Copy.mdb")
+ dim guidValue : guidValue = Mid(CreateObject("Scriptlet.TypeLib").Guid, 2, 36)
+ guidValue = Replace(guidValue, "-", "")
+ CreateDisposableDatabaseCopy = tempFolderPath & "\purple-envelope-tests-" & guidValue & ".mdb"
+ fso.CopyFile sourcePath, CreateDisposableDatabaseCopy, True
+
+ Set fso = Nothing
+ End Function
+
+ Private Sub UseDisposableDatabase(ByVal dbPath)
+ Set DAL__Singleton = Nothing
+ set DAL__Singleton = new Database_Class
+ DAL__Singleton.Initialize "Provider=Microsoft.Jet.OLEDB.4.0;Jet OLEDB:Engine Type=5;Data Source=" & dbPath & ";"
+ End Sub
+
+ Private Sub ResetDisposableDatabase()
+ dim tempPath : tempPath = m_tempDbPath
+ Set DAL__Singleton = Nothing
+
+ If Len(tempPath) > 0 Then
+ dim fso : set fso = Server.CreateObject("Scripting.FileSystemObject")
+ If fso.FileExists(tempPath) Then
+ fso.DeleteFile tempPath, True
+ End If
+ Set fso = Nothing
+ End If
+
+ m_tempDbPath = ""
+ End Sub
+
+ Private Function NextSeedKey(ByVal prefix)
+ NextSeedKey = prefix & Replace(Replace(Replace(CStr(Now()), "/", ""), ":", ""), " ", "") & CStr(Int((Rnd() * 1000000) + 1))
+ End Function
+
+ Private Function NextJurisdictionCode()
+ dim guidValue : guidValue = Mid(CreateObject("Scriptlet.TypeLib").Guid, 2, 8)
+ NextJurisdictionCode = "J" & Replace(guidValue, "-", "")
+ End Function
+
+ Private Function SeedJurisdiction(ByVal jCode, ByVal name)
+ DAL.Execute "INSERT INTO [Jurisdiction] ([JCode], [Name], [Mailing_Address], [CSZ], [IMB], [IMB_Digits]) VALUES (?,?,?,?,?,?)", _
+ Array(jCode, name, "123 Test St", "Lansing, MI 48933", "IMB", "123456789")
+ SeedJurisdiction = jCode
+ End Function
+
+ Private Function SeedPurpleEnvelopeKit(ByVal jobNumber, ByVal jCode, ByVal status)
+ DAL.Execute "INSERT INTO [Kit] ([JobNumber], [Jcode], [CreatedOn], [InkJetJob], [JobType], [Cass], [Status], [OutboundSTID], [InboundSTID], [OfficeCopiesAmount]) VALUES (?,?,?,?,?,?,?,?,?,?)", _
+ Array(jobNumber, jCode, Now(), False, "Purple Envelopes", False, status, "", "", Null)
+
+ SeedPurpleEnvelopeKit = QueryScalar("SELECT TOP 1 [ID] FROM [Kit] WHERE [JobNumber] = ? ORDER BY [ID] DESC", jobNumber)
+ End Function
+
+ Private Sub SeedInkjetRecord(ByVal kitId, ByVal precinct, ByVal ballotNumber, ByVal colorId)
+ DAL.Execute "INSERT INTO [InkjetRecords] ([KitID], [PRECINCT], [BALLOT_NUMBER], [ColorId]) VALUES (?,?,?,?)", _
+ Array(kitId, precinct, ballotNumber, colorId)
+ End Sub
+
+ Private Function QueryScalar(ByVal sql, ByVal params)
+ dim rs : set rs = DAL.Query(sql, params)
+ QueryScalar = rs(0).Value
+ Destroy rs
+ End Function
+
+ Private Function QueryPrecinctColorMap(ByVal kitId)
+ dim map : set map = Server.CreateObject("Scripting.Dictionary")
+ dim rs : set rs = DAL.Query("SELECT [PRECINCT], [ColorId] FROM [InkjetRecords] WHERE [KitID] = ? ORDER BY [PRECINCT]", kitId)
+
+ Do Until rs.EOF
+ If IsNull(rs("ColorId")) Then
+ map(rs("PRECINCT") & "") = ""
+ Else
+ map(rs("PRECINCT") & "") = CStr(rs("ColorId"))
+ End If
+ rs.MoveNext
+ Loop
+
+ Destroy rs
+ set QueryPrecinctColorMap = map
+ End Function
+
+ Public Sub Test_FormatElectionLabel_Returns_Mon_YYYY_For_Valid_Date(T)
+ T.AssertEqual "May-2026", PurpleEnvelopeReportHelper().FormatElectionLabel("5/26/2026"), "Expected valid dates to render as Mon-YYYY."
+ End Sub
+
+ Public Sub Test_FormatElectionLabel_Returns_Trimmed_Raw_Value_For_Invalid_Date(T)
+ T.AssertEqual "not a date", PurpleEnvelopeReportHelper().FormatElectionLabel(" not a date "), "Expected invalid election dates to fall back to trimmed raw text."
+ T.AssertEqual "", PurpleEnvelopeReportHelper().FormatElectionLabel(""), "Expected empty election dates to stay empty."
+ End Sub
+
+ Public Sub Test_SortPrecinctColorRows_Handles_Zero_Padded_And_Lettered_Precincts(T)
+ dim rs : set rs = CreatePrecinctColorRecordset(Array( _
+ Array("12B", "2"), _
+ Array("0003", ""), _
+ Array("A1", "4"), _
+ Array("12A", "3"), _
+ Array("3", "5"), _
+ Array("0001", "1"), _
+ Array("12", "6") _
+ ))
+ dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctColorRows(rs)
+
+ T.AssertEqual "0001,0003,3,12,12A,12B,A1", PrecinctListToCsv(sorted), "Expected mixed-format precincts to sort in the current ascending order."
+
+ Destroy rs
+ Destroy sorted
+ End Sub
+
+ Public Sub Test_SortPrecinctBallotRangeRows_Preserves_Ranges_While_Sorting(T)
+ dim rs : set rs = CreateBallotRangeRecordset(Array( _
+ Array("12A", 20, 29), _
+ Array("0003", 1, 10), _
+ Array("12", 11, 19), _
+ Array("A1", 30, 39) _
+ ))
+ dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(rs)
+ dim it : set it = sorted.Iterator()
+ dim row
+
+ T.AssertEqual "0003,12,12A,A1", PrecinctListToCsv(sorted), "Expected report rows to follow the same precinct order as the color table."
+
+ set row = it.GetNext()
+ T.AssertEqual 1, row.LowBallotNumber, "Expected the low ballot number to stay attached to precinct 0003."
+ T.AssertEqual 10, row.HighBallotNumber, "Expected the high ballot number to stay attached to precinct 0003."
+
+ Destroy rs
+ Destroy sorted
+ End Sub
+
+ Public Sub Test_GetPrecinctBallotRangesByKitId_Uses_Seeded_Data(T)
+ Randomize
+ dim jCode : jCode = NextJurisdictionCode()
+ dim jobNumber : jobNumber = NextSeedKey("JOB")
+ Call SeedJurisdiction(jCode, "City of Lansing")
+ dim kitId : kitId = SeedPurpleEnvelopeKit(jobNumber, jCode, "Ready To Assign STIDS")
+
+ SeedInkjetRecord kitId, "12A", "29", Null
+ SeedInkjetRecord kitId, "0003", "10", Null
+ SeedInkjetRecord kitId, "12A", "20", Null
+ SeedInkjetRecord kitId, "0003", "1", Null
+ SeedInkjetRecord kitId, "A1", "30", Null
+
+ dim rs : set rs = InkjetRecordsRepository.GetPrecinctBallotRangesByKitId(kitId)
+ dim sorted : set sorted = PurpleEnvelopeReportHelper().SortPrecinctBallotRangeRows(rs)
+ dim it : set it = sorted.Iterator()
+ dim firstRow, secondRow, thirdRow
+
+ set firstRow = it.GetNext()
+ set secondRow = it.GetNext()
+ set thirdRow = it.GetNext()
+
+ T.AssertEqual "0003,12A,A1", PrecinctListToCsv(sorted), "Expected seeded ballot ranges to sort in the current report order."
+ T.AssertEqual 1, firstRow.LowBallotNumber, "Expected precinct 0003 to use the seeded minimum ballot number."
+ T.AssertEqual 10, firstRow.HighBallotNumber, "Expected precinct 0003 to use the seeded maximum ballot number."
+ T.AssertEqual 20, secondRow.LowBallotNumber, "Expected precinct 12A to use the seeded minimum ballot number."
+ T.AssertEqual 29, secondRow.HighBallotNumber, "Expected precinct 12A to use the seeded maximum ballot number."
+ T.AssertEqual 30, thirdRow.LowBallotNumber, "Expected single-row precincts to keep their ballot number as the low value."
+ T.AssertEqual 30, thirdRow.HighBallotNumber, "Expected single-row precincts to keep their ballot number as the high value."
+
+ Destroy rs
+ Destroy sorted
+ Set firstRow = Nothing
+ Set secondRow = Nothing
+ Set thirdRow = Nothing
+ End Sub
+
+ Public Sub Test_UpdateColorForKit_Updates_All_Seeded_Rows_For_The_Target_Kit(T)
+ Randomize
+ dim jCode : jCode = NextJurisdictionCode()
+ Call SeedJurisdiction(jCode, "City of Lansing")
+
+ dim targetKitId : targetKitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS")
+ dim otherKitId : otherKitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS")
+
+ SeedInkjetRecord targetKitId, "0003", "1", 1
+ SeedInkjetRecord targetKitId, "12A", "2", Null
+ SeedInkjetRecord otherKitId, "9", "3", 4
+
+ InkjetRecordsRepository.UpdateColorForKit targetKitId, 7
+
+ dim targetMap : set targetMap = QueryPrecinctColorMap(targetKitId)
+ dim otherMap : set otherMap = QueryPrecinctColorMap(otherKitId)
+
+ T.AssertEqual "7", targetMap("0003"), "Expected kit-wide color updates to touch every row in the targeted kit."
+ T.AssertEqual "7", targetMap("12A"), "Expected kit-wide color updates to touch every row in the targeted kit."
+ T.AssertEqual "4", otherMap("9"), "Expected kit-wide color updates to leave other kits unchanged."
+
+ Set targetMap = Nothing
+ Set otherMap = Nothing
+ End Sub
+
+ Public Sub Test_UpdateColorForPrecinct_Updates_Only_The_Targeted_Precinct(T)
+ Randomize
+ dim jCode : jCode = NextJurisdictionCode()
+ Call SeedJurisdiction(jCode, "City of Lansing")
+
+ dim kitId : kitId = SeedPurpleEnvelopeKit(NextSeedKey("JOB"), jCode, "Ready To Assign STIDS")
+
+ SeedInkjetRecord kitId, "0003", "1", 1
+ SeedInkjetRecord kitId, "12A", "2", 2
+ SeedInkjetRecord kitId, "A1", "3", Null
+
+ InkjetRecordsRepository.UpdateColorForPrecinct kitId, "12A", 9
+
+ dim colorMap : set colorMap = QueryPrecinctColorMap(kitId)
+
+ T.AssertEqual "1", colorMap("0003"), "Expected non-targeted precincts to keep their original color."
+ T.AssertEqual "9", colorMap("12A"), "Expected the targeted precinct to receive the new color."
+ T.AssertEqual "", colorMap("A1"), "Expected non-targeted blank colors to remain blank."
+
+ Set colorMap = Nothing
+ End Sub
+
+ Public Sub Test_SwitchBoardPurpleEnvelopeEditFindById_Returns_Seeded_Header_Data(T)
+ Randomize
+ dim jCode : jCode = NextJurisdictionCode()
+ dim jobNumber : jobNumber = NextSeedKey("JOB")
+ Call SeedJurisdiction(jCode, "City of Lansing")
+ dim kitId : kitId = SeedPurpleEnvelopeKit(jobNumber, jCode, "Ready To Assign STIDS")
+
+ SeedInkjetRecord kitId, "0003", "1", Null
+ SeedInkjetRecord kitId, "12A", "2", Null
+
+ dim model : set model = KitRepository.SwitchBoardPurpleEnvelopeEditFindById(kitId)
+
+ T.AssertEqual kitId, model.ID, "Expected the seeded purple-envelope kit to be returned."
+ T.AssertEqual jobNumber, model.JobNumber, "Expected the seeded job number to be returned."
+ T.AssertEqual jCode, model.JCode, "Expected the seeded jurisdiction code to be returned."
+ T.AssertEqual "City of Lansing", model.Jurisdiction, "Expected the seeded jurisdiction name to be returned."
+ T.AssertEqual 2, model.LabelCount, "Expected the seeded inkjet row count to flow into the label count."
+ T.AssertEqual "Ready To Assign STIDS", model.Status, "Expected the seeded status to be returned."
+
+ Set model = Nothing
+ End Sub
+
+ Public Sub Test_KitController_Post_Actions_Still_Delegate_To_Color_Update_Repositories(T)
+ dim controllerSource : controllerSource = ReadAllText("../App/Controllers/Kit/KitController.asp")
+
+ T.Assert InStr(controllerSource, "Public Sub AssignKitColorPost") > 0, "Expected the kit-wide color POST action to remain present."
+ T.Assert InStr(controllerSource, "InkjetRecordsRepository.UpdateColorForKit CLng(ID), CLng(Request.Form(""KitColorId""))") > 0, "Expected AssignKitColorPost to keep delegating to the kit-wide color repository update."
+ T.Assert InStr(controllerSource, "Public Sub AssignPrecinctColorsPost") > 0, "Expected the precinct color POST action to remain present."
+ T.Assert InStr(controllerSource, "InkjetRecordsRepository.UpdateColorForPrecinct CLng(ID), precinct, CLng(colorId)") > 0, "Expected AssignPrecinctColorsPost to keep delegating to the precinct-specific color repository update."
+ End Sub
+
+ Public Sub Test_Report_View_Keeps_Print_Only_CSS_Contract(T)
+ dim viewSource : viewSource = ReadAllText("../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp")
+
+ T.Assert InStr(viewSource, "id=""purple-envelope-report-print""") > 0, "Expected the report print wrapper id to remain in the view."
+ T.Assert InStr(viewSource, "body * {") > 0, "Expected print CSS to hide non-report content."
+ T.Assert InStr(viewSource, "font-size: 10pt;") > 0, "Expected the reduced report font size to remain unchanged."
+ T.Assert InStr(viewSource, "padding: 0.45in 0.25in 0.25in 0.25in;") > 0, "Expected the print top buffer padding to remain unchanged."
+ End Sub
+
+ Public Sub Test_Report_View_Keeps_No_Data_Row_And_Page_Spacer(T)
+ dim viewSource : viewSource = ReadAllText("../App/Views/Kit/SwitchBoardPurpleEnvelopeEdit.asp")
+
+ T.Assert InStr(viewSource, "class=""print-page-spacer""") > 0, "Expected the repeated-page spacer row to remain in the report header."
+ T.Assert InStr(viewSource, "No precinct ballot data found for this kit.") > 0, "Expected the empty-state report message to remain unchanged."
+ End Sub
+End Class
+%>
diff --git a/Tests/Test_All.asp b/Tests/Test_All.asp
index ad7ca59..a2ea131 100644
--- a/Tests/Test_All.asp
+++ b/Tests/Test_All.asp
@@ -9,6 +9,7 @@ Option Explicit
+
<%
'Used in some of the test case classes included into this file.
@@ -26,5 +27,6 @@ Runner.AddTestContainer new AutoMap_Tests
Runner.AddTestContainer new FlexMap_Tests
Runner.AddTestContainer new DynMap_Tests
Runner.AddTestContainer new StringBuilder_Tests
+Runner.AddTestContainer new PurpleEnvelopeReport_Tests
Runner.Display
%>
diff --git a/Tests/TrackingDataImport_TestHarness.vbs b/Tests/TrackingDataImport_TestHarness.vbs
index ac83cdb..2115188 100644
Binary files a/Tests/TrackingDataImport_TestHarness.vbs and b/Tests/TrackingDataImport_TestHarness.vbs differ
diff --git a/Tests/chillkat_serial b/Tests/chillkat_serial
new file mode 100644
index 0000000..7200ce1
--- /dev/null
+++ b/Tests/chillkat_serial
@@ -0,0 +1 @@
+KENTCM.CB1022025_RGzBPM5J655e
\ No newline at end of file
diff --git a/_bmad-output/implementation-artifacts/tech-spec-exportinkjetfile-integration-test.md b/_bmad-output/implementation-artifacts/tech-spec-exportinkjetfile-integration-test.md
new file mode 100644
index 0000000..e4e7219
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/tech-spec-exportinkjetfile-integration-test.md
@@ -0,0 +1,300 @@
+---
+title: 'ExportInkjetFile Integration Test'
+slug: 'exportinkjetfile-integration-test'
+created: '2026-03-17'
+status: 'Implementation Complete'
+stepsCompleted: [1, 2, 3, 4]
+tech_stack: ['VBScript', 'ADODB', 'Chilkat_9_5_0.Csv', 'Access MDB/Jet']
+files_to_modify: ['Tests/TrackingDataImport_TestHarness.vbs']
+code_patterns: ['LoadFunctions+ExecuteGlobal isolation', 'ADODB connection to MDB', 'Chilkat CSV read-back']
+test_patterns: ['Integration test with real MDB fixture', 'Assert file output + DB side effect']
+---
+
+# Tech-Spec: ExportInkjetFile Integration Test
+
+**Created:** 2026-03-17
+
+## Overview
+
+### Problem Statement
+
+`ExportInkjetFile(KitID)` produces the inkjet operator CSV used on press day. It has zero automated coverage. The function joins InkjetRecords + KitLabels + Colors from an Access MDB, builds a Chilkat CSV with 22 columns, writes it to disk, then marks Kit.Status='Done'. A regression here causes silent bad output with no test catching it.
+
+### Solution
+
+Add an ADODB integration test directly to `Tests/TrackingDataImport_TestHarness.vbs`. The harness opens `Data/webdata - Copy.mdb` via ADODB, calls `ExportInkjetFile` on a real KitID, and asserts CSV structure (file exists, 22 columns, correct headers) plus ~10 row-level field values and the DB side-effect (Kit.Status='Done', InkJetJob=1).
+
+### Scope
+
+**In Scope:**
+- Set up integration test globals in harness: `oConn`, `ConnectionString` (pointing to `Data/webdata - Copy.mdb`), `ExportDirectory` (temp path under `Tests/`)
+- Load `GetSetting` and `ExportInkjetFile` into the `functionNames` array
+- Query MDB to discover a real KitID that has InkjetRecords
+- Call `ExportInkjetFile(KitID)`
+- Assert: CSV file exists at expected path
+- Assert: 22 column headers correct (spot-check cols 0, 5, 8, 11, 19, 20, 21)
+- Assert: CSV row count = InkjetRecords count for that KitID
+- Assert: First 10 rows — "Ballot Number" has leading zeros stripped, "Matching Code" starts with JCode
+- Assert: Kit.Status = 'Done' and Kit.InkJetJob = 1 after call
+- Cleanup: delete temp export folder
+
+**Out of Scope:**
+- Pure logic extraction from `ExportInkjetFile` (not viable — all field assembly is inline with recordset reads; refactoring would be required)
+- Seeding fixture data (real kit + inkjet records already exist in `Data/webdata - Copy.mdb`)
+- Office copies rows (separate conditional path; can be added later if a Kit with OfficeCopiesAmount > 0 is present)
+- Full row-by-row validation (10-record spot check is sufficient)
+- Restoring Kit.Status after test (MDB copy is disposable; original `webdata.mdb` is untouched)
+
+---
+
+## Context for Development
+
+### Codebase Patterns
+
+- **Harness pattern**: `LoadFunctions(filePath, functionNames)` extracts named functions from source via text parsing; `ExecuteGlobal` makes them available in harness global scope. Global vars set before calling functions are visible inside loaded functions.
+- **`LoadFunctions` is called at line 34**, before any test code. Adding `"GetSetting"` and `"ExportInkjetFile"` to the `functionNames` array (lines 16–32) is sufficient — they'll be extracted and available globally.
+- **`Set objFSO = fso` at line 36** — already set. `ExportInkjetFile` uses `objFSO` internally; this is already satisfied.
+- **`chilkatAvailable` declared at line 39** — already exists. New `integrationDbAvailable` follows the same declare-in-preamble, set-in-init-block pattern.
+- **Assertion API**: The harness has **no `Assert` sub** — only `AssertEqual(actual, expected, label)` and `AssertArrayEqual`. All integration test assertions must use `AssertEqual condition, True, "label"` form.
+- **DB dependency chain**: `ExportInkjetFile` calls `oConn.Open(ConnectionString)` if `oConn.State = 0`. It manages its own connection lifecycle. The harness must set `oConn` (via `Set oConn = CreateObject("ADODB.Connection")`) and `ConnectionString` as globals before calling.
+- **`ExportDirectory` global**: Used by `ExportInkjetFile` as-is (no trailing slash added internally). Must include trailing `\` in the harness assignment.
+- **Chilkat CSV write-back verification**: `ExportInkjetFile` creates its own internal `Chilkat_9_5_0.Csv` object. Chilkat unlock is process-wide — already done in harness init block. Read-back uses a separate object.
+
+### Files to Reference
+
+| File | Purpose |
+| ---- | ------- |
+| `Tests/TrackingDataImport_TestHarness.vbs` | Target file — 3 insertion points: globals (after line 15), `functionNames` array (after line 31), init block (after line 69), test block (before line 206) |
+| `ImportService/TrackingDataImport.vbs` | Source — `ExportInkjetFile` at line 251 (Function), `GetSetting` also in same file |
+| `Data/webdata - Copy.mdb` | MDB fixture — real Kit + InkjetRecords + KitLabels + Colors + Jurisdiction + Contacts + Settings |
+
+### Technical Decisions
+
+- **Use `Data/webdata - Copy.mdb` directly**: It's already a copy and contains real data. No need to seed a new fixture. The UPDATE side effect (Kit.Status='Done') is acceptable since this MDB is a disposable copy.
+- **Discover KitID at runtime via JOIN**: Query `SELECT TOP 1 ir.KitID FROM ((InkjetRecords ir INNER JOIN Kit k ON ir.KitID = k.ID) INNER JOIN Jurisdiction j ON k.JCode = j.JCode) INNER JOIN Contacts c ON k.JCode = c.JURISCODE` to ensure discovered KitID has all required related records. Avoid orphan-crash on `JurisdictionRs` or `ContactRs` inside the function.
+- **ExportDirectory as temp path**: Set to `fso.BuildPath(scriptDir, "export-test-output")`. **Delete before call** (not just after) to ensure no stale CSV from a prior failed run contaminates assertions.
+- **ADODB driver probe**: Try `Microsoft.ACE.OLEDB.12.0` first; fall back to `Microsoft.Jet.OLEDB.4.0`. If both fail, skip with message. Guard with `fso.FileExists(integrationMdbPath)` before attempting open.
+- **Load both `GetSetting` and `ExportInkjetFile`** in `functionNames` array — `GetSetting` is a DB-backed dependency called internally by `ExportInkjetFile`.
+- **Chilkat read-back**: Capture `verifyCsv.LoadFile(csvPath)` return value and assert it before proceeding to column/row assertions — prevents vacuously-passing assertions on a failed load.
+- **Cleanup is error-suppressed**: Wrap `fso.DeleteFolder` in `On Error Resume Next` / `On Error GoTo 0` — AV scanners may briefly hold a handle on the new CSV; a cleanup failure should not fail the test.
+
+---
+
+## Implementation Plan
+
+### Tasks
+
+- [x] **Task 1: Add integration test globals and ADODB probe block**
+ - File: `Tests/TrackingDataImport_TestHarness.vbs`
+ - Action: Insert 7 `Dim` declarations after line 15; insert ADODB probe block after line 69
+ - Notes: Sets `integrationDbAvailable`/`integrationDbSkipReason` — same guard pattern as `chilkatAvailable`
+
+**Insertion point: after line 15** (`Dim DataDirectory`). Add:
+
+```vbscript
+Dim oConn
+Dim ConnectionString
+Dim ExportDirectory
+Dim integrationMdbPath
+Dim integrationExportDir
+Dim integrationDbAvailable
+Dim integrationDbSkipReason
+```
+
+**Insertion point: after line 69** (`On Error GoTo 0` at end of Chilkat init block). Add ADODB probe block:
+
+```vbscript
+integrationMdbPath = fso.BuildPath(scriptDir, "..\Data\webdata - Copy.mdb")
+integrationExportDir = fso.BuildPath(scriptDir, "export-test-output")
+integrationDbAvailable = False
+integrationDbSkipReason = ""
+If Not fso.FileExists(integrationMdbPath) Then
+ integrationDbSkipReason = "MDB fixture not found: " & integrationMdbPath
+Else
+ Set oConn = CreateObject("ADODB.Connection")
+ On Error Resume Next
+ oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" & integrationMdbPath & ";"
+ If Err.Number <> 0 Then
+ Err.Clear
+ oConn.Open "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & integrationMdbPath & ";"
+ End If
+ If Err.Number <> 0 Then
+ integrationDbSkipReason = "No ADODB provider available (ACE and Jet both failed): " & Err.Description
+ Err.Clear
+ Else
+ integrationDbAvailable = True
+ ConnectionString = oConn.ConnectionString
+ ExportDirectory = integrationExportDir & "\"
+ End If
+ On Error GoTo 0
+ If oConn.State = 1 Then oConn.Close
+End If
+```
+
+- [x] **Task 2: Add `GetSetting` and `ExportInkjetFile` to `functionNames` array**
+ - File: `Tests/TrackingDataImport_TestHarness.vbs`
+ - Action: Extend array at lines 30–31 to include two new entries
+ - Notes: Must be done before `LoadFunctions` call at line 34; order within array does not matter
+
+**Insertion point: lines 30–31**. Change:
+```vbscript
+ "CheckStringDoesNotHaveForiegnCountries", _
+ "ValidImportCSV" _
+```
+To:
+```vbscript
+ "CheckStringDoesNotHaveForiegnCountries", _
+ "ValidImportCSV", _
+ "GetSetting", _
+ "ExportInkjetFile" _
+```
+
+- [x] **Task 3: Add ExportInkjetFile integration test block**
+ - File: `Tests/TrackingDataImport_TestHarness.vbs`
+ - Action: Insert full integration test block before line 206 (`WScript.Echo ""`)
+ - Notes: Guarded by `chilkatAvailable` AND `integrationDbAvailable`; uses `AssertEqual` throughout (no `Assert` sub exists); pre-call dir cleanup, post-call DB side-effect check, error-suppressed cleanup
+
+**Insertion point: before line 206** (`WScript.Echo ""`). Add:
+
+```vbscript
+' === ExportInkjetFile Integration Test ===
+If Not chilkatAvailable Then
+ WScript.Echo "SKIP: ExportInkjetFile integration test (Chilkat unavailable)"
+ElseIf Not integrationDbAvailable Then
+ WScript.Echo "SKIP: ExportInkjetFile integration test (" & integrationDbSkipReason & ")"
+Else
+ oConn.Open ConnectionString
+
+ Dim kitDiscoverRs
+ Set kitDiscoverRs = oConn.Execute( _
+ "SELECT TOP 1 ir.KitID FROM ((InkjetRecords ir " & _
+ "INNER JOIN Kit k ON ir.KitID = k.ID) " & _
+ "INNER JOIN Jurisdiction j ON k.JCode = j.JCode) " & _
+ "INNER JOIN Contacts c ON k.JCode = c.JURISCODE;")
+ If kitDiscoverRs.EOF Then
+ WScript.Echo "SKIP: ExportInkjetFile — no complete Kit+InkjetRecords+Jurisdiction+Contact found in fixture MDB"
+ oConn.Close
+ Else
+ Dim testKitID : testKitID = kitDiscoverRs("KitID").Value
+
+ Dim countRs
+ Set countRs = oConn.Execute("SELECT COUNT(*) AS N FROM InkjetRecords WHERE KitID=" & testKitID & ";")
+ Dim expectedRows : expectedRows = countRs("N").Value
+
+ Dim kitRsCheck
+ Set kitRsCheck = oConn.Execute("SELECT JCode FROM Kit WHERE ID=" & testKitID & ";")
+ Dim testJCode : testJCode = kitRsCheck("JCode").Value
+
+ oConn.Close
+
+ ' Delete stale output before call, then recreate
+ On Error Resume Next
+ If fso.FolderExists(integrationExportDir) Then fso.DeleteFolder(integrationExportDir, True)
+ On Error GoTo 0
+ fso.CreateFolder integrationExportDir
+
+ ExportInkjetFile testKitID
+
+ Dim exportSubfolders : Set exportSubfolders = fso.GetFolder(integrationExportDir).SubFolders
+ Dim csvFound : csvFound = False
+ Dim csvPath : csvPath = ""
+ Dim sf
+ For Each sf In exportSubfolders
+ Dim csvFiles : Set csvFiles = sf.Files
+ Dim f
+ For Each f In csvFiles
+ If LCase(fso.GetExtensionName(f.Name)) = "csv" Then
+ csvFound = True
+ csvPath = f.Path
+ End If
+ Next
+ Next
+
+ AssertEqual csvFound, True, "[INT] ExportInkjetFile: CSV file created"
+
+ If csvFound Then
+ Dim verifyCsv : Set verifyCsv = CreateObject("Chilkat_9_5_0.Csv")
+ verifyCsv.HasColumnNames = 1
+ Dim csvLoaded : csvLoaded = verifyCsv.LoadFile(csvPath)
+ AssertEqual csvLoaded, True, "[INT] ExportInkjetFile: CSV loaded by Chilkat"
+
+ If csvLoaded Then
+ AssertEqual verifyCsv.NumColumns, 22, "[INT] ExportInkjetFile: 22 columns"
+ AssertEqual verifyCsv.ColumnName(0), "Full Name", "[INT] ExportInkjetFile: col 0 = Full Name"
+ AssertEqual verifyCsv.ColumnName(5), "IM barcode Characters", "[INT] ExportInkjetFile: col 5 = IM barcode Characters"
+ AssertEqual verifyCsv.ColumnName(8), "Ballot Number", "[INT] ExportInkjetFile: col 8 = Ballot Number"
+ AssertEqual verifyCsv.ColumnName(11), "Combined Pct_Ballot Num", "[INT] ExportInkjetFile: col 11 = Combined Pct_Ballot Num"
+ AssertEqual verifyCsv.ColumnName(19), "Matching Code", "[INT] ExportInkjetFile: col 19 = Matching Code"
+ AssertEqual verifyCsv.ColumnName(20), "ColorFilepath", "[INT] ExportInkjetFile: col 20 = ColorFilepath"
+ AssertEqual verifyCsv.ColumnName(21), "ColorName", "[INT] ExportInkjetFile: col 21 = ColorName"
+ AssertEqual verifyCsv.NumRows, expectedRows, "[INT] ExportInkjetFile: row count matches InkjetRecords"
+
+ Dim checkRows : checkRows = 10
+ If verifyCsv.NumRows < 10 Then checkRows = verifyCsv.NumRows
+ Dim r
+ For r = 0 To checkRows - 1
+ Dim ballotNum : ballotNum = verifyCsv.GetCell(r, 8)
+ AssertEqual (Left(ballotNum, 1) <> "0" Or ballotNum = ""), True, "[INT] ExportInkjetFile: row " & r & " Ballot Number no leading zeros"
+ Dim matchCode : matchCode = verifyCsv.GetCell(r, 19)
+ AssertEqual Left(matchCode, Len(testJCode)), testJCode, "[INT] ExportInkjetFile: row " & r & " Matching Code starts with JCode"
+ Next
+ End If
+
+ Set verifyCsv = Nothing
+ End If
+
+ oConn.Open ConnectionString
+ Dim sideEffectRs
+ Set sideEffectRs = oConn.Execute("SELECT Status, InkJetJob FROM Kit WHERE ID=" & testKitID & ";")
+ AssertEqual sideEffectRs("Status").Value, "Done", "[INT] ExportInkjetFile: Kit.Status = Done"
+ AssertEqual sideEffectRs("InkJetJob").Value, 1, "[INT] ExportInkjetFile: Kit.InkJetJob = 1"
+ oConn.Close
+
+ On Error Resume Next
+ If fso.FolderExists(integrationExportDir) Then fso.DeleteFolder(integrationExportDir, True)
+ On Error GoTo 0
+ End If
+End If
+```
+
+- [x] **Task 4: Verify `Option Explicit` compliance**
+ - File: `Tests/TrackingDataImport_TestHarness.vbs`
+ - Action: Review all new variable names introduced in Tasks 1–3 and confirm each has a `Dim` declaration
+ - Notes: New inline `Dim`s in Task 3: `kitDiscoverRs`, `testKitID`, `countRs`, `expectedRows`, `kitRsCheck`, `testJCode`, `exportSubfolders`, `csvFound`, `csvPath`, `sf`, `csvFiles`, `f`, `verifyCsv`, `csvLoaded`, `checkRows`, `r`, `ballotNum`, `matchCode`, `sideEffectRs` — all must be present
+
+### Acceptance Criteria
+
+- [ ] **AC 1:** Given Chilkat is available and `Data/webdata - Copy.mdb` contains a Kit with InkjetRecords, Jurisdiction, and Contact, when the harness calls `ExportInkjetFile(testKitID)`, then a CSV file is created under `Tests/export-test-output/-/`, it has exactly 22 columns with correct header names at indices 0/5/8/11/19/20/21, row count matches the InkjetRecords count, the first 10 rows have no leading zeros in "Ballot Number", the first 10 rows have "Matching Code" starting with JCode, and Kit.Status='Done' + Kit.InkJetJob=1 in the MDB.
+
+- [ ] **AC 2:** Given Chilkat is unavailable, when the harness reaches the ExportInkjetFile block, then it prints `SKIP: ExportInkjetFile integration test (Chilkat unavailable)` and no test failures are recorded.
+
+- [ ] **AC 3:** Given `Data/webdata - Copy.mdb` is absent or neither ACE nor Jet ADODB driver is available, when the ADODB probe block runs during harness init, then `integrationDbAvailable` is False, the test block prints a SKIP message with the reason, and no test failures are recorded.
+
+- [ ] **AC 4:** Given the MDB contains no Kit with all required related records (InkjetRecords + Jurisdiction + Contacts), when the discovery JOIN query runs, then it prints `SKIP: ExportInkjetFile — no complete Kit+InkjetRecords+Jurisdiction+Contact found in fixture MDB` and no test failures are recorded.
+
+- [ ] **AC 5:** Given a prior test run left `Tests/export-test-output/` behind (simulating a failed cleanup), when the integration test runs, then the stale folder is deleted before `ExportInkjetFile` is called and the test proceeds without contaminated assertions.
+
+---
+
+## Additional Context
+
+### Dependencies
+
+- `Chilkat_9_5_0.Csv` COM — must be registered and unlocked (guarded by `chilkatAvailable`)
+- `Microsoft.ACE.OLEDB.12.0` or `Microsoft.Jet.OLEDB.4.0` — must be available for ADODB to open the MDB
+- `Data/webdata - Copy.mdb` — must exist with real Kit + InkjetRecords + KitLabels + Colors + Jurisdiction + Contacts + Settings(ElectionDate)
+- `ADODB.Connection` — available on any Windows machine with Office/Access drivers
+
+### Testing Strategy
+
+Integration test only — no unit-level testing is viable for this function without significant refactoring. The test confirms the full output pipeline from DB read → CSV write → DB update.
+
+### Notes
+
+- The `ExportInkjetFile` function uses `objFSO` (not `fso`). The line `Set objFSO = fso` was added in a prior workflow pass and is already present in the harness.
+- Column index 11 has a double-space in its name: `"Combined Pct_Ballot Num"` — this is as-written in the source (line 296).
+- The Chilkat CSV column indices: source sets cols 0–21 (22 columns total). The `ColumnName(n)` method on the read-back CSV object uses 0-based index.
+- **ADODB driver probe**: Init block tries ACE first (`Microsoft.ACE.OLEDB.12.0`), falls back to Jet (`Microsoft.Jet.OLEDB.4.0`). Bitness matters — 32-bit cscript requires 32-bit drivers. Sets `integrationDbAvailable = True/False` and `integrationDbSkipReason`.
+- **oConn lifecycle**: The probe block opens and immediately closes `oConn` to validate the connection string. `ExportInkjetFile` internally calls `oConn.Open(ConnectionString)` if State=0 — so close it before calling the function. Reopen after for the side-effect assertion.
+- **KitID discovery JOIN**: Uses 4-table JOIN (InkjetRecords + Kit + Jurisdiction + Contacts) to guarantee no orphan-crash inside `ExportInkjetFile` when it accesses `JurisdictionRs("Name")` or `ContactRs("Title")`.
+- **Pre-call cleanup**: `integrationExportDir` is deleted before calling `ExportInkjetFile` (not just after) to prevent stale CSVs from prior failed runs contaminating assertions.
diff --git a/_bmad-output/test-artifacts/automation-summary.md b/_bmad-output/test-artifacts/automation-summary.md
new file mode 100644
index 0000000..85801fa
--- /dev/null
+++ b/_bmad-output/test-artifacts/automation-summary.md
@@ -0,0 +1,187 @@
+---
+stepsCompleted:
+ - step-01-preflight-and-context
+ - step-02-identify-targets
+ - step-03-generate-tests
+ - step-03c-aggregate
+ - step-04-validate-and-summarize
+lastStep: step-04-validate-and-summarize
+lastSaved: 2026-03-17
+workflowType: testarch-automate
+target: ImportService/TrackingDataImport.vbs
+---
+
+# Test Automation Expansion: TrackingDataImport.vbs
+
+## Step 1: Preflight & Context
+
+### Stack Detection
+
+- `test_stack_type`: auto → **detected: `backend`**
+- No frontend tooling (no `package.json`, no Playwright/Cypress config)
+- No conventional backend manifests — project is **VBScript / Classic ASP**
+- Framework for this component: **standalone VBScript test harness** (`Tests/TrackingDataImport_TestHarness.vbs`) run via `cscript`
+
+### Execution Mode
+
+**Standalone** — source code only; no story/PRD/test-design artifacts exist for this component.
+
+### TEA Config Flags
+
+- `tea_use_playwright_utils`: true → **Not applicable** (no browser/HTTP layer)
+- `tea_use_pactjs_utils`: true → **Not applicable** (no service contracts)
+- `tea_pact_mcp`: mcp → **Not applicable**
+- `tea_browser_automation`: auto → **Not applicable** (script only)
+
+### Existing Test Harness Summary
+
+`Tests/TrackingDataImport_TestHarness.vbs` uses `LoadFunctions` + `ExecuteGlobal` to extract individual functions from the source and test them in isolation. Current state:
+
+| Function | Status | Test Count |
+|---|---|---|
+| `Truncate` | ✅ Tested | 2 |
+| `PadLeft` | ✅ Tested | 2 |
+| `PadString` | ✅ Tested | 3 (including Null input) |
+| `CleanNull` | ✅ Tested | 2 |
+| `CompressArray` | ✅ Tested | 1 |
+| `TrimLeadingZeros` | ✅ Tested | 2 |
+| `PushNonEmptyToBottom` | ✅ Tested | 1 |
+| `GetState` | ✅ Tested | 2 |
+| `GetCityFromLine` | ✅ Tested | 3 (including Null) |
+| `Assign` | ⚠️ Loaded, not tested | 0 |
+| `Choice` | ⚠️ Loaded, not tested | 0 |
+| `CheckForFiles` | ⚠️ Smoke only (empty dir) | 1 |
+| `CheckStringDoesNotHaveForiegnCountries` | ❌ Not in harness | 0 |
+
+### Functions Out of Scope (Require COM/DB/Shell)
+
+`ValidJcode`, `GetSetting`, `CheckStatusFor`, `CheckForJobsToCass`, `ValidImportCSV`, `ConvertCsvToString`, `SetupKit`, `ImportCass`, `ExportMMCsv`, `RunMailManager`, `CreateExportForSnailWorks`, `CreateProofForJurisdiction`, `createTrackingInfoForKit`, `ExportInkjetFile`, `ThereAreCustomOfficeCopyJobsReady`, `CreateCustomOfficeCopyJobsInKjetFiles`, `CreateCustomOfficeCopyJobsProofFiles`, `CheckSnailWorksPurpleEnvelopeExport`, `CheckSnailWorksTrakingKitExport`, `Main`, `InitConfig`, `ProcessStatus`
+
+---
+
+## Step 2: Coverage Plan
+
+### ⚠️ Highest Actual Risk — Zero Coverage (Orchestration Path)
+
+The import pipeline `CheckForFiles` → `ConvertCsvToString` → `ValidImportCSV` → `SetupKit` has **no automated coverage at any level**. This is the path that processes real CSV files into the database. A failure here causes silent data loss or incorrect kit creation. The unit tests below are baseline documentation; they do not protect this pipeline.
+
+### Targets by Test Level
+
+**Unit (VBScript `cscript` harness) — only applicable level for this component**
+
+#### P0 — Critical: Import gate + behavior-documentation
+
+| Target | Gap | Test Cases | Note |
+|---|---|---|---|
+| `ValidImportCSV` | Import gate — accepts/rejects all incoming files; Chilkat COM likely available in harness | 20-column CSV → True, 19-column → False, 0-column → False | Investigate: harness can `CreateObject("Chilkat_9_5_0.Csv")` directly |
+| `CheckStringDoesNotHaveForiegnCountries` | Not in harness; pure logic, no COM dependency | clean US address → True, `"CANADA"` → False, `"JAPAN"` → False, lowercase `"canada"` → True (documents case-sensitive behavior) | Substring test `"123 Norway Ave"` → True documents assumption that input arrives pre-uppercased |
+| `GetState` | `IgnoreCase=False` — mixed-case CSV input silently returns no state | `"lansing, mi 48906"` → `""` (documents real behavior risk) | Affects downstream CASS processing |
+
+#### P0 — Failure mode cases (surfaced by FMA)
+
+| Target | Case | Expected | Risk Note |
+|---|---|---|---|
+| `CheckStringDoesNotHaveForiegnCountries` | Null input | type mismatch (documents crash) | `InStr(Null,x)` raises runtime error |
+| `GetState` | Null input | type mismatch | `RegExp.Execute(Null)` raises type mismatch |
+| `Choice` | Null condition | False branch taken silently | VBScript evaluates `If Null Then` as False |
+
+#### P1 — Important: Edge cases for already-tested functions
+
+| Target | Missing Edge Cases |
+|---|---|
+| `TrimLeadingZeros` | `""` → `""`, `"abc"` → `"abc"`, `"0"` → `""`, `" 007"` (leading space) → `" 007"`, Null → type mismatch |
+| `PadLeft` | exact-length `"007"` → `"007"`, empty string → `"000"`, Null → type mismatch |
+| `PadString` | longer-than-size `"abcde"` → `"abcde"`, `""` → `" "` |
+| `CompressArray` | all-empty array, all-non-empty array, single-element array |
+| `GetState` | `""` → `""` |
+
+#### P2 — Low priority (loaded but trivially low-risk)
+
+| Target | Test Cases | Note |
+|---|---|---|
+| `Assign` | scalar string, scalar number, empty string, zero | 5-line utility; tests are baseline documentation only |
+| `Choice` | True branch, False branch, computed True/False | Calls `Assign`; tests document expected delegation |
+| `CheckForFiles` deeper | Requires Chilkat CSV COM for valid-file path | Skip unless `ValidImportCSV` Chilkat investigation succeeds |
+
+### Justification
+
+`ValidImportCSV` is the import gate — every CSV import decision flows through it. Its column-count check (`NumColumns = 20`) is testable if `Chilkat_9_5_0.Csv` can be instantiated in the harness via `CreateObject`. `CheckStringDoesNotHaveForiegnCountries` and `GetState` document real behavior risks around case sensitivity and substring matching in production CSV data. `Assign`/`Choice` are downgraded to P2 — baseline documentation, not safety-critical coverage.
+
+---
+
+## Step 3: Generated Tests
+
+### File Modified
+
+`Tests/TrackingDataImport_TestHarness.vbs` — updated in-place (VBScript harness pattern, no new files required)
+
+### Changes Applied
+
+**New globals added:**
+- `Dim objCSV` — makes Chilkat CSV available to `ValidImportCSV`
+- `Set objFSO = fso` — ensures `objFSO` is set early for loaded functions
+- `Dim chilkatAvailable` + COM probe block — skips `ValidImportCSV` tests gracefully if Chilkat not registered
+
+**New entries in `functionNames` array:**
+- `"CheckStringDoesNotHaveForiegnCountries"`
+- `"ValidImportCSV"`
+
+**New test assertions (38 new tests):**
+
+| Priority | Function | Cases |
+|---|---|---|
+| P0 | `CheckStringDoesNotHaveForiegnCountries` | 7 (clean US, CANADA, JAPAN, Norway Ave, lowercase canada, empty, Null) |
+| P0 | `GetState` | 2 (lowercase miss, empty) |
+| P0 | `Choice` | 1 (Null condition) |
+| P0 | `ValidImportCSV` | 3 (20-col accept, 19-col reject, empty reject) — conditional on Chilkat |
+| P1 | `TrimLeadingZeros` | 4 (empty, non-numeric, single zero, leading space) |
+| P1 | `PadLeft` | 2 (exact length, empty string) |
+| P1 | `PadString` | 2 (longer-than-size, empty string) |
+| P1 | `CompressArray` | 3 (all-empty, all-non-empty, single-element) |
+| P2 | `Assign` | 4 (string, number, empty, zero) |
+| P2 | `Choice` | 4 (True, False, computed True, computed False) |
+
+**Total new tests: 32** (+ 3 conditional on Chilkat availability = 35 max)
+
+### Test Count Summary
+
+| | Count |
+|---|---|
+| Existing tests (before this pass) | 19 |
+| New tests added | 32 (+3 conditional) |
+| **Total (Chilkat available)** | **54** |
+| **Total (Chilkat unavailable)** | **51** |
+
+## Validation
+
+### Checklist Result: ✅ PASS
+
+All applicable items passed. Full checklist adapted for VBScript `cscript` harness (Playwright/TypeScript items marked N/A).
+
+**Key verifications:**
+- `Choice(Null, "yes", "no")` → `"no"` confirmed: VBScript `If Null Then` evaluates False
+- `CheckStringDoesNotHaveForiegnCountries(Null)` → `True` confirmed: `InStr(Null, x)` returns Null (falsy in If)
+- `ValidImportCSV` guard pattern correct: `chilkatAvailable` flag skips gracefully if COM absent
+- All `Dim` declarations satisfy `Option Explicit` requirement
+- No shared mutable state between test groups
+
+### Assumptions and Risks
+
+- `ValidImportCSV` tests depend on `Chilkat_9_5_0.Csv` COM registration on the machine running the harness. The guard skips them cleanly if absent.
+- The orchestration path (`CheckForFiles` → `ConvertCsvToString` → `ValidImportCSV` → `SetupKit`) remains **without automated coverage** — this is the highest actual risk. Database-backed integration tests would require a seeded MDB fixture and are out of scope for the cscript harness pattern.
+- `CheckStringDoesNotHaveForiegnCountries` substring and case-sensitivity tests are **behavior-documentation tests**, not defect tests. They document the assumption that upstream CSV data arrives pre-uppercased.
+- `GetState` lowercase test documents a known production risk: mixed-case city/state lines in real CSV data silently return no state.
+
+### How to Run
+
+```
+cscript Tests\TrackingDataImport_TestHarness.vbs
+```
+
+Expected output (Chilkat available): `Passed: 54, Failed: 0`
+Expected output (Chilkat unavailable): `SKIP: ValidImportCSV...` then `Passed: 51, Failed: 0`
+
+### Recommended Next Workflow
+
+- `/bmad-tea-bmad-testarch-test-review` — review coverage quality and identify remaining gaps
+- Optional follow-up: add seeded MDB integration tests for the `CheckForFiles` → `SetupKit` pipeline using a disposable MDB copy (highest actual risk, currently uncovered)
diff --git a/_bmad-output/test-artifacts/test-design-architecture.md b/_bmad-output/test-artifacts/test-design-architecture.md
new file mode 100644
index 0000000..d90f824
--- /dev/null
+++ b/_bmad-output/test-artifacts/test-design-architecture.md
@@ -0,0 +1,248 @@
+---
+stepsCompleted:
+ - step-01-detect-mode
+ - step-02-load-context
+ - step-03-risk-and-testability
+ - step-04-coverage-plan
+ - step-05-generate-output
+lastStep: step-05-generate-output
+lastSaved: 2026-03-17
+workflowType: testarch-test-design
+inputDocuments:
+ - D:\Development\Tracking_Kits\docs\project-overview.md
+ - D:\Development\Tracking_Kits\docs\architecture.md
+ - D:\Development\Tracking_Kits\docs\module-map.md
+ - D:\Development\Tracking_Kits\docs\development-guide.md
+ - D:\Development\Tracking_Kits\_bmad-output\project-context.md
+ - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp
+ - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp
+ - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp
+---
+
+# Test Design for Architecture: Purple Envelope Edit and Ballot Range Report
+
+**Purpose:** Architectural concerns, testability gaps, and NFR requirements for review by Dev/Architecture before adding regression tests for the current purple-envelope workflow.
+
+**Date:** 2026-03-17
+**Author:** Daniel Covington
+**Status:** Architecture Review Pending
+**Project:** workspace
+**PRD Reference:** Brownfield functional references: `docs/project-overview.md`, `docs/module-map.md`, and the currently deployed purple-envelope behavior
+**ADR Reference:** Brownfield architecture references: `docs/architecture.md` and `App/Controllers/Kit/KitController.asp`
+
+---
+
+## Executive Summary
+
+**Scope:** Preserve current behavior for `SwitchBoardPurpleEnvelopeEdit`, including report rendering, mixed-format precinct ordering, election-date formatting, color-assignment forms, and print-only report scoping.
+
+**Business Context**:
+
+- **Revenue/Impact:** Operational election-mail output; wrong report data can cause incorrect ballot packaging and operator rework.
+- **Problem:** The feature is currently exercised mostly through manual IIS verification, so small controller/view/repository regressions can silently change report output.
+- **GA Launch:** Immediate brownfield regression protection for current production behavior.
+
+**Architecture**:
+
+- **Key Decision 1:** Purple-envelope behavior remains in Classic ASP controller/view/repository layers, not a separate service.
+- **Key Decision 2:** Report content is rendered as HTML from `KitController` + `InkjetRecordsRepository` + `SwitchBoardPurpleEnvelopeEdit.asp`.
+- **Key Decision 3:** Mixed-format precinct ordering is handled in VBScript controller helpers, while ballot range aggregation is still SQL-backed.
+
+**Expected Scale**:
+
+- Single-kit page render with precinct rows sourced from `InkjetRecords`
+- Operator-facing print workflow from browser print preview
+- Existing automated test harness limited to ASPUnit helper-style tests
+
+**Risk Summary:**
+
+- **Total risks:** 7
+- **High-priority (>=6):** 3 risks requiring mitigation before relying on automated regression coverage
+- **Test effort:** ~14-19 targeted automated checks plus 2 manual print checks (~1-1.5 QA weeks)
+
+---
+
+## Quick Guide
+
+### BLOCKERS - Team Must Decide
+
+1. **R-006: No isolated fixture path for Kit/Inkjet/Settings data** - Tests need a disposable MDB or deterministic seed/reset strategy before meaningful controller/repository regression coverage can be added. Recommended owner: Dev.
+2. **R-002: Ballot range correctness is data-sensitive** - We need agreed fixture data covering numeric, zero-padded, and alpha-suffixed precincts plus min/max ballot scenarios. Recommended owner: Dev + QA.
+3. **R-001: Mixed-format precinct ordering is custom logic** - The current order rules must be frozen as explicit expected outputs before tests are written. Recommended owner: Product/Dev/QA.
+
+### HIGH PRIORITY - Team Should Validate
+
+1. **R-003: Election date formatting/fallback behavior** - Approve the rule set: valid dates render `Mon-YYYY`, invalid or missing values fall back without crashing.
+2. **R-004: Print-only report scope depends on browser print behavior** - Approve a split strategy: automated HTML/CSS assertions plus manual browser print smoke for final confidence.
+3. **R-005: Color-assignment regressions can hide behind the same screen** - Validate that the report tests must also preserve existing `AssignKitColorPost` and `AssignPrecinctColorsPost` behavior.
+
+### INFO ONLY - Solutions Provided
+
+1. **Test strategy:** favor repository/controller integration checks plus limited manual IIS print verification.
+2. **Tooling:** reuse ASPUnit, a disposable test MDB copy, and HTML response assertions.
+3. **Coverage:** focus first on sorting, aggregation, date formatting, same-screen regression, and print scoping.
+4. **Gate guidance:** P0 automated checks must pass 100%; P1 >=95%; manual IIS print smoke required before release for print-impacting changes.
+
+---
+
+## For Architects and Devs - Open Topics
+
+### Risk Assessment
+
+**Total risks identified:** 7 (3 high-priority score >=6, 4 medium, 0 low)
+
+#### High-Priority Risks (Score >=6) - Immediate Attention
+
+| Risk ID | Category | Description | Probability | Impact | Score | Mitigation | Owner | Timeline |
+| --- | --- | --- | --- | --- | --- | --- | --- | --- |
+| **R-001** | **DATA** | Mixed-format precinct sort order changes (`0003`, `12A`, alpha-only values) and produces wrong operator-facing sequence | 2 | 3 | **6** | Lock expected order with seeded regression fixtures and HTML assertions | Dev + QA | Before next release |
+| **R-002** | **DATA** | Ballot range query/regression returns wrong low or high ballot numbers per precinct | 2 | 3 | **6** | Add repository-level aggregation checks against known datasets | Dev + QA | Before next release |
+| **R-006** | **TECH** | No isolated test-data harness for `Kit`, `InkjetRecords`, `Settings`, and `Colors` blocks stable regression coverage | 3 | 2 | **6** | Create disposable MDB copy and reset/seeding conventions for tests | Dev | Before automated suite work |
+
+#### Medium-Priority Risks (Score 3-5)
+
+| Risk ID | Category | Description | Probability | Impact | Score | Mitigation | Owner |
+| --- | --- | --- | --- | --- | --- | --- | --- |
+| R-003 | BUS | Election date header shows wrong format or blank value unexpectedly | 2 | 2 | 4 | Add valid, invalid, and missing-setting render checks | QA |
+| R-004 | OPS | Print-only output regresses and shows extra page content or loses repeated-page spacing | 2 | 2 | 4 | Add HTML/CSS assertions and manual print smoke | QA |
+| R-005 | BUS | Same-screen changes break existing color-assignment workflows while report tests are added | 2 | 2 | 4 | Add regression checks for both POST actions | Dev + QA |
+| R-007 | BUS | Empty precinct data renders broken or confusing output instead of the current no-data row | 2 | 2 | 4 | Add explicit empty-state render test | QA |
+
+#### Risk Category Legend
+
+- **TECH**: architectural or testability risk
+- **DATA**: incorrect ballot or precinct data interpretation
+- **BUS**: operator-facing workflow correctness
+- **OPS**: environment or print/runtime behavior
+
+---
+
+### Testability Concerns and Architectural Gaps
+
+#### 1. Blockers to Fast Feedback
+
+| Concern | Impact | What Architecture Must Provide | Owner | Timeline |
+| --- | --- | --- | --- | --- |
+| **No disposable test database pattern** | Automated tests will share mutable data and become brittle | A documented temp MDB copy/reset approach for ASPUnit-backed integration tests | Dev | Pre-implementation |
+| **No render-test seam for controller output** | Report assertions stay manual and expensive | A small harness or convention for invoking `SwitchBoardPurpleEnvelopeEdit` against seeded data and asserting HTML | Dev | Pre-implementation |
+| **Implicit behavior contract only lives in code/user feedback** | Tests may encode the wrong business rule | A short frozen rules list for sorting, header formatting, and empty-state expectations | Dev + QA + Stakeholders | Pre-implementation |
+
+#### 2. Architectural Improvements Needed
+
+1. **Stabilize feature fixtures**
+ - **Current problem:** no reusable seed set for mixed precinct and ballot-range cases.
+ - **Required change:** create named fixture datasets for numeric-only, zero-padded, alpha-suffixed, alpha-only, and empty-state kits.
+ - **Impact if not fixed:** tests will either miss key edge cases or rely on hand-built ad hoc data.
+ - **Owner:** Dev
+ - **Timeline:** before automated test implementation
+
+2. **Separate deterministic logic from page orchestration where practical**
+ - **Current problem:** ordering/date helpers are private controller methods, making low-level regression checks harder.
+ - **Required change:** either expose deterministic helper seams or cover the helpers through stable page-render assertions.
+ - **Impact if not fixed:** coverage will skew toward broad render tests only.
+ - **Owner:** Dev
+ - **Timeline:** implementation phase if needed
+
+3. **Document browser-dependent print boundaries**
+ - **Current problem:** browser headers/footers and repeated-page spacing are only partially controllable in app code.
+ - **Required change:** record which print expectations are automatable versus manual.
+ - **Impact if not fixed:** release disputes on “test passed but print preview changed”.
+ - **Owner:** QA
+ - **Timeline:** before sign-off
+
+---
+
+### Testability Assessment Summary
+
+#### What Works Well
+
+- Existing repository boundaries make ballot-range aggregation testable without full browser automation.
+- The report HTML is deterministic from seeded data, which is good for response-body assertions.
+- The feature is localized to `KitController`, `InkjetRecordsRepository`, `KitViewModels`, and one view template.
+- ASPUnit already exists, so the project has a place to add targeted regression checks.
+
+#### Accepted Trade-offs
+
+- Browser-generated print headers/footers will remain manual-verification territory.
+- Full visual print fidelity on Windows/IIS is accepted as a release smoke check, not a unit-level assertion target.
+
+---
+
+### Risk Mitigation Plans (High-Priority Risks >=6)
+
+#### R-001: Mixed-format precinct sort regression (Score: 6) - HIGH
+
+**Mitigation Strategy:**
+
+1. Define the expected canonical sort order for representative precinct values.
+2. Seed one kit containing `0001`, `0003`, `12`, `12A`, `12B`, and alpha-only precincts.
+3. Assert that both the color table and report table render in the same expected order.
+
+**Owner:** Dev + QA
+**Timeline:** Before next release
+**Status:** Planned
+**Verification:** Automated HTML assertions against the seeded render output
+
+#### R-002: Ballot range aggregation regression (Score: 6) - HIGH
+
+**Mitigation Strategy:**
+
+1. Seed multiple `InkjetRecords` rows per precinct with known min/max ballot numbers.
+2. Assert repository output for each precinct’s low/high values.
+3. Assert the rendered HTML shows the same values and no cross-precinct bleed.
+
+**Owner:** Dev + QA
+**Timeline:** Before next release
+**Status:** Planned
+**Verification:** Repository integration checks plus page-render verification
+
+#### R-006: Missing isolated test-data harness (Score: 6) - HIGH
+
+**Mitigation Strategy:**
+
+1. Stand up a disposable test MDB copy for ASPUnit/controller integration runs.
+2. Create reset hooks for `Kit`, `InkjetRecords`, `Settings`, and `Colors`.
+3. Document fixture naming and cleanup rules for parallel-safe future tests.
+
+**Owner:** Dev
+**Timeline:** Before automated suite expansion
+**Status:** Planned
+**Verification:** Test runs can seed, execute, and reset without polluting shared data
+
+---
+
+### Assumptions and Dependencies
+
+#### Assumptions
+
+1. Brownfield docs plus current user-approved behavior are sufficient substitutes for a formal PRD for this feature slice.
+2. IIS-backed manual verification remains available for final print checks.
+3. Existing status strings and form routing remain part of the contract and should not be normalized during test work.
+
+#### Dependencies
+
+1. Disposable test data store available before controller/repository automation begins.
+2. Mixed-format precinct fixture definitions agreed before expected-order assertions are written.
+3. QA has access to a Windows/IIS browser for manual print smoke validation.
+
+#### Risks to Plan
+
+- **Risk:** undocumented operator expectations about sort order
+ - **Impact:** the suite may freeze the wrong behavior
+ - **Contingency:** validate the expected fixture output with stakeholders before coding assertions
+
+---
+
+**End of Architecture Document**
+
+**Next Steps for Architecture/Dev Team:**
+
+1. Approve the sort-order and date-format behavior contract.
+2. Provide a disposable MDB fixture/reset approach.
+3. Confirm manual print checks remain part of release validation for this feature.
+
+**Next Steps for QA Team:**
+
+1. Use the companion QA doc for concrete scenario planning.
+2. Build seeded datasets around mixed-format precincts and ballot ranges.
+3. Add manual print preview checks only for browser-dependent behavior.
diff --git a/_bmad-output/test-artifacts/test-design-progress.md b/_bmad-output/test-artifacts/test-design-progress.md
new file mode 100644
index 0000000..9c1c084
--- /dev/null
+++ b/_bmad-output/test-artifacts/test-design-progress.md
@@ -0,0 +1,80 @@
+---
+stepsCompleted:
+ - step-01-detect-mode
+ - step-02-load-context
+ - step-03-risk-and-testability
+ - step-04-coverage-plan
+ - step-05-generate-output
+lastStep: step-05-generate-output
+lastSaved: 2026-03-17
+workflowType: testarch-test-design
+inputDocuments:
+ - D:\Development\Tracking_Kits\docs\project-overview.md
+ - D:\Development\Tracking_Kits\docs\architecture.md
+ - D:\Development\Tracking_Kits\docs\module-map.md
+ - D:\Development\Tracking_Kits\docs\development-guide.md
+ - D:\Development\Tracking_Kits\_bmad-output\project-context.md
+ - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp
+ - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp
+ - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp
+ - D:\Development\Tracking_Kits\Tests\Test_All.asp
+ - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\adr-quality-readiness-checklist.md
+ - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\risk-governance.md
+ - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\probability-impact.md
+ - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-levels-framework.md
+ - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-priorities-matrix.md
+ - D:\Development\Tracking_Kits\_bmad\tea\testarch\knowledge\test-quality.md
+---
+
+# Test Design Progress
+
+## Step 1 - Detect Mode
+
+- Selected **system-level mode**
+- Reason: user requested a risk-based plan for current functionality, no sprint-status artifact exists, and brownfield architecture/docs were available
+- Constraint noted: no formal PRD/ADR exists for this feature slice, so brownfield docs and current approved behavior were used as the functional baseline
+
+## Step 2 - Load Context
+
+- Loaded project context, module docs, development/testing guidance, and the exact purple-envelope controller/repository/view files
+- Loaded TEA knowledge fragments for ADR readiness, risk scoring, probability/impact, test levels, priorities, and test quality
+- Confirmed current test framework is ASPUnit with helper-focused coverage, not browser E2E automation
+
+## Step 3 - Risk and Testability
+
+- Identified 7 risks total, with 3 high-priority risks:
+ - R-001 mixed-format precinct ordering regression
+ - R-002 ballot range aggregation regression
+ - R-006 missing isolated test-data harness
+- Identified primary testability blockers:
+ - no disposable test DB convention
+ - no controller render harness
+ - behavior contract is mostly implicit in code and operator expectations
+
+## Step 4 - Coverage Plan
+
+- Defined a focused regression plan:
+ - P0 ~4 checks
+ - P1 ~6 checks
+ - P2 ~4 checks
+ - P3 ~2 manual print checks
+- Chose integration-style ASPUnit/IIS verification as the main automated level
+- Kept print-preview specifics as manual smoke for browser-controlled behavior
+
+## Step 5 - Generate Output
+
+- Wrote architecture-focused test design:
+ - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md`
+- Wrote QA execution plan:
+ - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md`
+- Wrote BMAD handoff:
+ - `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design\workspace-handoff.md`
+
+## Completion Report
+
+- **Mode used:** system-level
+- **Key gate thresholds:** P0 100% pass, P1 >=95%, manual Windows print smoke for print-affecting changes
+- **Open assumptions:**
+ - brownfield docs are acceptable substitutes for a formal PRD
+ - a disposable MDB/reset approach will be created before automation work starts
+ - browser header/footer behavior remains outside reliable app-level automation
diff --git a/_bmad-output/test-artifacts/test-design-qa.md b/_bmad-output/test-artifacts/test-design-qa.md
new file mode 100644
index 0000000..7651c9a
--- /dev/null
+++ b/_bmad-output/test-artifacts/test-design-qa.md
@@ -0,0 +1,311 @@
+---
+stepsCompleted:
+ - step-01-detect-mode
+ - step-02-load-context
+ - step-03-risk-and-testability
+ - step-04-coverage-plan
+ - step-05-generate-output
+lastStep: step-05-generate-output
+lastSaved: 2026-03-17
+workflowType: testarch-test-design
+inputDocuments:
+ - D:\Development\Tracking_Kits\docs\project-overview.md
+ - D:\Development\Tracking_Kits\docs\architecture.md
+ - D:\Development\Tracking_Kits\docs\module-map.md
+ - D:\Development\Tracking_Kits\docs\development-guide.md
+ - D:\Development\Tracking_Kits\_bmad-output\project-context.md
+ - D:\Development\Tracking_Kits\App\Controllers\Kit\KitController.asp
+ - D:\Development\Tracking_Kits\App\DomainModels\InkjetRecordsRepository.asp
+ - D:\Development\Tracking_Kits\App\Views\Kit\SwitchBoardPurpleEnvelopeEdit.asp
+ - D:\Development\Tracking_Kits\Tests\Test_All.asp
+---
+
+# Test Design for QA: Purple Envelope Edit and Ballot Range Report
+
+**Purpose:** Test execution recipe for QA and devs adding regression coverage for the current purple-envelope edit/report workflow.
+
+**Date:** 2026-03-17
+**Author:** Daniel Covington
+**Status:** Draft
+**Project:** workspace
+
+**Related:** See `test-design-architecture.md` for architectural blockers and testability gaps.
+
+---
+
+## Executive Summary
+
+**Scope:** Preserve the current behavior of the purple-envelope edit screen, including report content, mixed-format precinct ordering, election-date rendering, color assignment, empty states, and print-only markup boundaries.
+
+**Risk Summary:**
+
+- Total Risks: 7 (3 high-priority, 4 medium)
+- Critical Categories: DATA, TECH, BUS
+
+**Coverage Summary:**
+
+- P0 tests: ~4
+- P1 tests: ~6
+- P2 tests: ~4
+- P3 tests: ~2 manual checks
+- **Total:** ~16 targeted checks plus 2 manual browser-print checks (~1-1.5 QA weeks)
+
+---
+
+## Not in Scope
+
+| Item | Reasoning | Mitigation |
+| --- | --- | --- |
+| **ReportMan/PDF exports** | Not part of this feature slice; separate export path and COM dependencies | Covered by existing manual export verification |
+| **Browser-generated print headers/footers** | Controlled by browser settings, not reliably by page code | Manual print smoke checklist documents expected operator setup |
+| **Jurisdiction import or unrelated kit workflows** | Outside the current purple-envelope regression target | Existing module-level validation remains separate |
+
+---
+
+## Dependencies & Test Blockers
+
+### Backend/Architecture Dependencies (Pre-Implementation)
+
+1. **Disposable test MDB strategy** - Dev - Pre-implementation
+ - QA needs a resettable database copy or equivalent seed/reset routine.
+ - Without this, controller/repository tests will be brittle and non-repeatable.
+
+2. **Seed dataset for mixed-format precincts** - Dev + QA - Pre-implementation
+ - QA needs representative data for `000x`, numeric, alpha-suffixed, alpha-only, and empty-state precinct cases.
+ - Without this, ordering and aggregation checks will not cover the risky paths.
+
+3. **Controller render harness convention** - Dev - Pre-implementation
+ - QA needs a repeatable way to exercise `SwitchBoardPurpleEnvelopeEdit` and inspect the HTML body under IIS/ASPUnit-compatible flow.
+ - Without this, only repository tests will be automated and the view contract stays manual.
+
+### QA Infrastructure Setup (Pre-Implementation)
+
+1. **ASPUnit extension points**
+ - Add one focused test container for purple-envelope regression checks.
+ - Keep tests deterministic and data-seeded.
+
+2. **Test data fixtures**
+ - Fixture set A: ballot ranges with known low/high numbers.
+ - Fixture set B: mixed-format precinct ordering.
+ - Fixture set C: missing/invalid `Electiondate`.
+
+3. **Windows/IIS smoke environment**
+ - One browser-print smoke pass for report-only printing and repeated-page top spacing.
+
+---
+
+## Risk Assessment
+
+### High-Priority Risks (Score >=6)
+
+| Risk ID | Category | Description | Score | QA Test Coverage |
+| --- | --- | --- | --- | --- |
+| **R-001** | DATA | Mixed-format precinct ordering changes | **6** | Seeded render assertions for both color table and report table |
+| **R-002** | DATA | Wrong low/high ballot aggregation per precinct | **6** | Repository aggregation tests plus rendered-value checks |
+| **R-006** | TECH | No isolated fixture/reset path for stable tests | **6** | Test harness readiness criteria and pre-test reset validation |
+
+### Medium/Low-Priority Risks
+
+| Risk ID | Category | Description | Score | QA Test Coverage |
+| --- | --- | --- | --- | --- |
+| R-003 | BUS | Election date renders incorrectly or crashes on bad input | 4 | Valid/missing/invalid setting render checks |
+| R-004 | OPS | Print-only scope or top spacing regresses | 4 | HTML/CSS assertions plus manual print smoke |
+| R-005 | BUS | Color-assignment actions regress while report tests are added | 4 | POST regression checks for kit-wide and precinct-specific updates |
+| R-007 | BUS | Empty precinct state breaks current report contract | 4 | Explicit no-data render check |
+
+---
+
+## Entry Criteria
+
+- [ ] Disposable test MDB or equivalent reset strategy available
+- [ ] Mixed-format precinct fixtures agreed and documented
+- [ ] IIS-accessible test path for controller/render verification available
+- [ ] Existing `Tests/Test_All.asp` runner still executes successfully
+- [ ] Manual Windows browser available for final print smoke
+
+## Exit Criteria
+
+- [ ] All P0 checks passing
+- [ ] All P1 checks passing or formally triaged
+- [ ] No open high-risk regression on ordering, aggregation, or data header behavior
+- [ ] HTML render contract validated for empty state and report-only print container
+- [ ] Manual print smoke completed for report-only output and repeated-page spacing
+
+---
+
+## Test Coverage Plan
+
+**Note:** P0/P1/P2/P3 represent priority and risk, not execution timing.
+
+### P0 (Critical)
+
+**Criteria:** Preserves operator-facing data correctness and high-risk regression paths with no acceptable workaround.
+
+| Test ID | Requirement | Test Level | Risk Link | Notes |
+| --- | --- | --- | --- | --- |
+| **P0-001** | Report shows correct jurisdiction/JCode and formatted election label for seeded valid data | Integration | R-003 | Assert rendered HTML content |
+| **P0-002** | Mixed-format precincts render in the approved ascending order | Integration | R-001 | Cover `000x`, numeric, alpha-suffixed, alpha-only |
+| **P0-003** | Each precinct shows correct low/high ballot numbers from seeded records | Integration | R-002 | Assert repository output and rendered HTML |
+| **P0-004** | Empty precinct dataset renders current no-data message instead of broken markup | Integration | R-007 | Protect empty-state contract |
+
+**Total P0:** ~4 tests
+
+---
+
+### P1 (High)
+
+**Criteria:** Preserves important same-screen workflows and print-related regressions with moderate-to-high operational impact.
+
+| Test ID | Requirement | Test Level | Risk Link | Notes |
+| --- | --- | --- | --- | --- |
+| **P1-001** | `AssignKitColorPost` updates all `InkjetRecords.ColorId` rows for the kit | Integration | R-005 | Seeded DB verification |
+| **P1-002** | `AssignPrecinctColorsPost` updates only the targeted precinct rows | Integration | R-005 | Ensure no cross-precinct bleed |
+| **P1-003** | Missing `Electiondate` does not error and leaves report date blank | Integration | R-003 | Assert safe fallback |
+| **P1-004** | Invalid `Electiondate` value preserves non-crashing fallback behavior | Integration | R-003 | Use non-date string fixture |
+| **P1-005** | Print container isolates the report section in markup/CSS | Integration | R-004 | Assert presence of `#purple-envelope-report-print` rules |
+| **P1-006** | `Ready To Assign STIDS` status still gates the existing form block | Integration | R-005 | Regression for same-screen conditional behavior |
+
+**Total P1:** ~6 tests
+
+---
+
+### P2 (Medium)
+
+**Criteria:** Lower-risk formatting and regression checks that still help freeze today’s behavior.
+
+| Test ID | Requirement | Test Level | Risk Link | Notes |
+| --- | --- | --- | --- | --- |
+| **P2-001** | Full-year format renders as `Mon-YYYY` for valid election dates | Integration | R-003 | Example `May-2026` |
+| **P2-002** | Print spacer row and top buffer CSS remain present | Integration | R-004 | Markup/CSS contract only |
+| **P2-003** | Report font-size contract remains at current reduced print size | Integration | R-004 | CSS assertion |
+| **P2-004** | Color table and report table stay aligned on precinct order | Integration | R-001 | Prevent divergent ordering paths |
+
+**Total P2:** ~4 tests
+
+---
+
+### P3 (Low)
+
+**Criteria:** Manual, browser-dependent, or release-smoke-only validation.
+
+| Test ID | Requirement | Test Level | Notes |
+| --- | --- | --- | --- |
+| **P3-001** | Browser print preview shows only the report section | Manual Browser | Requires operator print settings |
+| **P3-002** | Page 2+ maintains visible top breathing room in print preview | Manual Browser | Validate on Windows/IIS browser |
+
+**Total P3:** ~2 checks
+
+---
+
+## Execution Strategy
+
+### Every PR: ASPUnit + seeded render/repository checks (~5-10 min)
+
+- Run the existing `Tests/Test_All.asp` suite
+- Run the new purple-envelope regression container against disposable seeded data
+- Keep automated checks focused on repository correctness and rendered HTML contract
+
+### Nightly: expanded seeded regression pass (~10-20 min)
+
+- Re-run the purple-envelope suite against larger mixed-format datasets
+- Include any fixture-reset verification and broader same-screen regression checks
+
+### Weekly or Pre-Release: manual Windows/IIS print smoke (~15-30 min)
+
+- Confirm report-only printing
+- Confirm repeated-page top spacing
+- Confirm browser-dependent behavior still matches operator expectations
+
+---
+
+## QA Effort Estimate
+
+| Priority | Count | Effort Range | Notes |
+| --- | --- | --- | --- |
+| P0 | ~4 | ~8-12 hours | Core seeded render/aggregation checks |
+| P1 | ~6 | ~10-16 hours | POST regressions, fallbacks, print markup |
+| P2 | ~4 | ~6-10 hours | Formatting and alignment checks |
+| P3 | ~2 | ~2-4 hours | Manual print verification |
+| **Total** | ~16 | **~26-42 hours** | **~1-1.5 QA weeks** |
+
+**Assumptions:**
+
+- Disposable fixture/reset path is provided early.
+- Tests reuse existing ASPUnit conventions instead of introducing a new framework.
+- Manual print validation remains intentionally small and release-focused.
+
+---
+
+## Implementation Planning Handoff
+
+| Work Item | Owner | Target Milestone (Optional) | Dependencies/Notes |
+| --- | --- | --- | --- |
+| Add disposable MDB copy/reset process for purple-envelope tests | Dev | Sprint next | Required before automation |
+| Add repository regression tests for ballot range aggregation | Dev/QA | Sprint next | Uses fixture set A |
+| Add controller/render assertions for report HTML and sort order | Dev/QA | Sprint next | Uses fixture sets A/B/C |
+| Add manual print smoke checklist to release flow | QA | Pre-release | Browser-dependent only |
+
+---
+
+## Tooling & Access
+
+| Tool or Service | Purpose | Access Required | Status |
+| --- | --- | --- | --- |
+| IIS-hosted local app | Execute controller/render tests | Windows + IIS site access | Ready |
+| Disposable MDB copy | Stable seeded data | File-system access to test DB copy | Pending |
+| ASPUnit runner | Automated regression execution | Existing `Tests/Test_All.asp` access | Ready |
+| Windows browser print preview | Manual print smoke | Local browser access | Ready |
+
+**Access requests needed (if any):**
+
+- [ ] Disposable test database path and reset ownership confirmed
+
+---
+
+## Interworking & Regression
+
+| Service/Component | Impact | Regression Scope | Validation Steps |
+| --- | --- | --- | --- |
+| **KitController** | Orchestrates page model and POST actions | Existing edit page behavior plus report additions | Render and POST regression checks |
+| **InkjetRecordsRepository** | Supplies precinct lists, aggregation, and color updates | Ordering-sensitive and data-sensitive queries | Seeded repository assertions |
+| **SettingsRepository** | Supplies `Electiondate` | Header formatting/fallback | Valid/missing/invalid setting cases |
+| **SwitchBoardPurpleEnvelopeEdit.asp** | Final HTML/print contract | Report-only container, table content, empty state | HTML response assertions + manual print smoke |
+
+**Regression test strategy:**
+
+- Existing `Tests/Test_All.asp` must still pass.
+- New seeded regression checks should run before release on any change touching the purple-envelope screen, repository, or print CSS.
+- Manual Windows print smoke remains required for print-affecting changes.
+
+---
+
+## Appendix A: Code Examples & Tagging
+
+For this repo, prefer ASPUnit naming aligned to the existing suite rather than Playwright tagging:
+
+- `PurpleEnvelopeReport_Should_Render_ElectionDate`
+- `PurpleEnvelopeReport_Should_Sort_MixedPrecincts_Ascending`
+- `PurpleEnvelopeReport_Should_Show_BallotRanges_PerPrecinct`
+- `AssignPrecinctColors_Should_Update_Only_TargetPrecinct`
+
+Keep tests deterministic:
+
+- Seed explicit fixture data
+- Assert HTML or repository outputs directly
+- Avoid browser timing waits in automated checks
+
+---
+
+## Appendix B: Knowledge Base References
+
+- `risk-governance.md`
+- `probability-impact.md`
+- `test-levels-framework.md`
+- `test-priorities-matrix.md`
+- `test-quality.md`
+
+---
+
+**Generated by:** BMad TEA Agent
+**Workflow:** `_bmad/tea/testarch/bmad-testarch-test-design`
+**Mode:** System-level (brownfield feature scope)
diff --git a/_bmad-output/test-artifacts/test-design/workspace-handoff.md b/_bmad-output/test-artifacts/test-design/workspace-handoff.md
new file mode 100644
index 0000000..43669b6
--- /dev/null
+++ b/_bmad-output/test-artifacts/test-design/workspace-handoff.md
@@ -0,0 +1,85 @@
+---
+title: TEA Test Design -> BMAD Handoff Document
+version: "1.0"
+workflowType: testarch-test-design-handoff
+inputDocuments:
+ - D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md
+ - D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md
+sourceWorkflow: testarch-test-design
+generatedBy: TEA Master Test Architect
+generatedAt: "2026-03-17T00:00:00"
+projectName: workspace
+---
+
+# TEA -> BMAD Integration Handoff
+
+## Purpose
+
+This document bridges the purple-envelope regression test design with future BMAD epic/story work so the current behavior contract is preserved during implementation.
+
+## TEA Artifacts Inventory
+
+| Artifact | Path | BMAD Integration Point |
+| --- | --- | --- |
+| Test Design Architecture | `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-architecture.md` | Architectural blockers, risk mitigation, testability requirements |
+| Test Design QA | `D:\Development\Tracking_Kits\_bmad-output\test-artifacts\test-design-qa.md` | Story acceptance criteria and regression scenario coverage |
+| Risk Assessment | embedded in both docs | Epic quality gates and story priority |
+
+## Epic-Level Integration Guidance
+
+### Risk References
+
+- **R-001 / R-002** must be treated as epic-level quality blockers because ordering or aggregation defects change operator-facing output.
+- **R-006** must be scheduled as enabling work before expecting reliable automated regression coverage.
+
+### Quality Gates
+
+- No story touching the purple-envelope edit page is complete until mixed-format precinct ordering is proven against seeded data.
+- No story touching the report query/view is complete until ballot low/high aggregation is verified.
+- Print-affecting changes require a manual Windows/IIS print smoke before release.
+
+## Story-Level Integration Guidance
+
+### P0/P1 Test Scenarios -> Story Acceptance Criteria
+
+- The purple-envelope report renders jurisdiction, JCode, and election date exactly as currently approved.
+- Precincts sort in the frozen ascending order for zero-padded, numeric, and alpha-suffixed values.
+- Each precinct shows the correct low/high ballot number range.
+- Kit-wide and precinct-specific color updates still work on the same screen.
+- Empty precinct datasets show the current no-data row without error.
+
+### Data-TestId Requirements
+
+- Not applicable for Classic ASP in the current implementation.
+- Prefer stable HTML markers, predictable headings, and table structure for server-render assertions.
+
+## Risk-to-Story Mapping
+
+| Risk ID | Category | P×I | Recommended Story/Epic | Test Level |
+| --- | --- | --- | --- | --- |
+| R-001 | DATA | 2x3 | Purple-envelope report ordering contract | Integration |
+| R-002 | DATA | 2x3 | Purple-envelope ballot aggregation contract | Integration |
+| R-003 | BUS | 2x2 | Election date render/fallback behavior | Integration |
+| R-004 | OPS | 2x2 | Print-only report markup and manual print smoke | Integration + Manual |
+| R-005 | BUS | 2x2 | Preserve color-assignment workflows on same screen | Integration |
+| R-006 | TECH | 3x2 | Disposable MDB/reset harness for regression suite | Enabler |
+| R-007 | BUS | 2x2 | Empty-state render contract | Integration |
+
+## Recommended BMAD -> TEA Workflow Sequence
+
+1. TEA Test Design (`TD`) -> complete
+2. BMAD Create Epics & Stories -> include regression-preservation stories and test-harness enabler work
+3. TEA ATDD (`AT`) -> generate failing checks for P0 report-ordering and aggregation scenarios
+4. BMAD Implementation -> add fixtures/tests/harness work
+5. TEA Automate (`TA`) -> expand the suite beyond the initial P0/P1 checks
+6. TEA Trace (`TR`) -> verify risk coverage completeness
+
+## Phase Transition Quality Gates
+
+| From Phase | To Phase | Gate Criteria |
+| --- | --- | --- |
+| Test Design | Epic/Story Creation | R-001, R-002, and R-006 mitigation work represented in planning |
+| Epic/Story Creation | ATDD | P0 scenarios for ordering and aggregation translated into executable checks |
+| ATDD | Implementation | Seed fixtures and disposable DB approach agreed |
+| Implementation | Test Automation | Automated P0 checks passing |
+| Test Automation | Release | Manual print smoke complete for print-affecting changes |