TASK-3.2: Implement Wafer Loop / Cassette Cadence
- Status: Proposed (no passes started)
- Date: 2026-05-08
- Spec: SLICE-3.2: Cassette Cadence
- Depends on: TASK-3.3: SQLite Persistence (run-history persistence + migration runner), TASK-3.1: Rich Defect Model (defect persistence + AppState.RecentDefects), TASK-1.6: FlaUI Capture (the rig that captures the row block)
Objective
Compose SLICE-3.3 + SLICE-3.1 + the existing workflow state machine into a wafer-cassette scheduler that runs 25 wafers back-to-back through Load → Align → Run → Unload phases. Add WaferId and LotId to RunSummary. Introduce a stub-row-at-run-start pattern so defects.run_id FK can be enforced (closing the SLICE-3.1 follow-up #3). Add cassette UI panel showing slot progress. 25-wafer cassette completes under Soak8h profile in ≤ 20 min wall-clock with all per-wafer records persisted and zero FK errors.
Scope
- New domain shapes (
WaferId,LotId,CassetteSlot,CassetteState,WaferPhase) inDomain.Contracts RunTerminalStatus.Pendingenum valueRunSummarygains nullableWaferId/LotIdstrings- M003 migration (
wafer_id+lot_idcolumns + indexes); M004 migration (FK-retirement audit marker) ICassetteScheduler(Application.Abstractions) +SimulatedCassetteScheduler(Infrastructure.Simulator)WorkflowService.StartRunAsync(WaferId? = null, LotId? = null)overload; stub-row insert at run start; existing terminal-state update at run endSqliteDefectStore.OpenAsyncremovesPRAGMA foreign_keys = OFF(FK band-aid retired)AppState.Cassettefield;MainViewModel.CassetteStatusPanelprojectionSimulatorProfilegainsWaferLoadMs/WaferAlignMs/WaferUnloadMsCassetteStatusPanelUserControl + 4 new MainWindow buttons with AutomationIds- New
MeasurementExtraction.psm1helpers:Get-WafersCompletedCount,Get-CassetteWallClockSeconds - Runbook §5.4 entry; row block in
phase-3-measurements.md - Tests: cassette scheduler loop, stub-row pattern, FK orphan-row integrity (zero orphans), reproducibility against
slice-1-1-multi-tag-telemetry, 25-wafer cassette acceptance
Non-Scope
- Multi-cassette / lot-batching beyond 25 wafers
- Real wafer-handler robotics (Load / Align / Unload are simulated
Task.Delay) - Pre-align failure modes (notch not found, wafer mis-clamp)
- Mid-cassette pause / resume across cassette boundaries
- Wafer-level retry policy (operator re-loads slot manually)
OperatorId— Phase 3.4 territory- Defect correlation across wafers (cross-wafer pattern detection is Phase 4 ML)
- Cassette UI virtualization (25 slots is small)
- Modifying
DefectSeverity/RunTerminalStatusother than addingPending
Touched Projects
src/InspectionPrototype.Domain— newWaferId.cs,LotId.cs,CassetteSlot.cs,CassetteState.cs,WaferPhase.cs;RunTerminalStatus.csaddsPending;RunSummary.csadds two nullable string fieldssrc/InspectionPrototype.Application— newAbstractions/ICassetteScheduler.cs;State/AppState.csaddsCassette;Services/WorkflowService.cs(stub-row insert + StartRunAsync overload)src/InspectionPrototype.Infrastructure— newData/Migrations/M003_cassette_columns.sql+M004_enable_defects_fk.sql; newSimulator/SimulatedCassetteScheduler.cs;Simulator/SimulatorProfilesOptions.csadds 3 fields;Data/SqliteRunHistoryStore.cs(extended INSERT for new columns);Data/SqliteDefectStore.cs(remove PRAGMA OFF); DI wiringsrc/InspectionPrototype.Application/State/SimulatorProfile.cs— 3 new fields with defaultssrc/InspectionPrototype.Presentation— newControls/CassetteStatusPanel.xaml/.cs; newViewModels/CassetteSlotViewModel.cs;MainViewModelcassette projectionsrc/InspectionPrototype.App/MainWindow.xaml— new buttons with AutomationIds; CassetteStatusPanel hostingtests/InspectionPrototype.Tests—MigrationRunnerM003M004Tests,SimulatedCassetteSchedulerTests,WorkflowServiceStubRowTests,SqliteDefectStoreFkIntegrationTests,MainViewModelCassetteProjectionTests;Stubs/FakeCassetteScheduler.cstests/Tools/MeasurementExtraction.Tests.ps1— Pester for two new helpersdocs/runbook/capturing-measurements.md— §5.4docs/reviews/phase-3-measurements.md— new row blockdocs/captures/— new CSVtools/MeasurementExtraction.psm1— two new helpers
AI Tool Guidance
Three Copilot passes; one-pass-per-session.
- Domain shapes + persistence + scheduler + WorkflowService stub-row + FK retirement. All non-UI code; M003 + M004; SimulatedCassetteScheduler; tests including the FK orphan-row integrity test. NO UI work, NO captures.
- Cassette UI panel.
CassetteStatusPanelUserControl +CassetteSlotViewModelprojection in MainViewModel + 4 new MainWindow buttons + AutomationIds + UI tests. NO captures. - 25-wafer cassette capture under Soak8h + row block + runbook §5.4 + Phase 2 trigger assessment. No code changes.
Acceptance Criteria Mapping
The implementation must satisfy all acceptance criteria from SLICE-3.2:
- Pass 1 covers criteria 1–10, 17, 18, and the C#-test portions of 12
- Pass 2 covers criteria 9 (UI projection portion), 11
- Pass 3 covers criteria 12 (capture-driven verification), 13, 14, 15, 16
Copilot Agent Prompts
Pass 1 — Domain shapes + persistence + scheduler + WorkflowService stub-row + FK retirement
You are implementing Pass 1 of TASK-3.2: introduce the wafer-cassette
scheduler, add stub-row-at-run-start pattern that closes the SLICE-3.1 FK
band-aid, and persist WaferId/LotId on RunSummary. NO UI work (Pass 2). NO
captures (Pass 3).
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-3.2-cassette-cadence.md (the requirements)
- docs/tasks/TASK-3.2-implement-cassette-cadence.md (this task)
- docs/specs/SLICE-3.3-sqlite-persistence.md (parallel patterns)
- docs/specs/SLICE-3.1-rich-defect-model.md (FK band-aid context)
- src/InspectionPrototype.Application/Services/WorkflowService.cs
- src/InspectionPrototype.Application/Services/AppStateStore.cs
- src/InspectionPrototype.Domain/Contracts/RunSummary.cs
- src/InspectionPrototype.Domain/Contracts/RunTerminalStatus.cs
- src/InspectionPrototype.Infrastructure/Data/Migrations/M001_initial_schema.sql
and M002_defects.sql (migration patterns)
- src/InspectionPrototype.Infrastructure/Data/SqliteRunHistoryStore.cs
- src/InspectionPrototype.Infrastructure/Data/SqliteDefectStore.cs
(the FK band-aid line — to be retired)
- src/InspectionPrototype.Application/State/AppState.cs
- src/InspectionPrototype.Application/State/SimulatorProfile.cs
- src/InspectionPrototype.Infrastructure/Simulator/SimulatorProfilesOptions.cs
## Scope of this pass
All non-UI code: domain shapes + migrations + scheduler + WorkflowService
extension + FK retirement + AppState shape + tests. NO UI changes.
## Deliverables
1. Domain shapes (src/InspectionPrototype.Domain/Contracts/):
- WaferPhase.cs:
public enum WaferPhase { Idle, Loading, Aligning, Running, Unloading, Complete }
- WaferId.cs: record with Value string + Generate(lotId, slotIndex) static
- LotId.cs: record with Value string + GenerateForToday() static (yyyyMMdd-HHmmss)
- CassetteSlot.cs: record (Index, WaferId?)
- CassetteState.cs: record (LotId, TotalSlots, CurrentSlotIndex, CurrentPhase, IReadOnlyList<CassetteSlot> Slots)
- RunTerminalStatus.cs: add Pending value as the FIRST enum value (so default(RunTerminalStatus) = Pending)
2. RunSummary extension (src/InspectionPrototype.Domain/Contracts/RunSummary.cs):
Add at end of record:
string? WaferId = null,
string? LotId = null
3. M003 SQL embedded resource
(src/InspectionPrototype.Infrastructure/Data/Migrations/M003_cassette_columns.sql):
ALTER TABLE run_summaries ADD COLUMN wafer_id TEXT;
ALTER TABLE run_summaries ADD COLUMN lot_id TEXT;
CREATE INDEX idx_run_summaries_lot_id ON run_summaries(lot_id);
CREATE INDEX idx_run_summaries_wafer_id ON run_summaries(wafer_id);
INSERT INTO schema_version (version, applied_at) VALUES (3, datetime('now'));
4. M004 SQL embedded resource (FK-retirement audit marker)
(src/InspectionPrototype.Infrastructure/Data/Migrations/M004_enable_defects_fk.sql):
-- M004 — no schema change. Marks the version bump where SqliteDefectStore
-- removed its `PRAGMA foreign_keys = OFF` band-aid (retired by introducing
-- the stub-row pattern in WorkflowService.StartRunAsync). Defect inserts
-- now succeed under FK enforcement because the parent run_summary row
-- exists before any defect is persisted.
INSERT INTO schema_version (version, applied_at) VALUES (4, datetime('now'));
5. SqliteRunHistoryStore.SaveAsync — extend INSERT to include new columns:
- INSERT INTO run_summaries (..., wafer_id, lot_id) VALUES (..., @WaferId, @LotId)
- Map RunSummary.WaferId / LotId in the parameter object.
- Update the SELECT statements (LoadRecentAsync / LoadPageAsync / GetAsync) to
include wafer_id and lot_id columns; map back via DTO + MapRow.
- DefectRow's RunSummaryRow DTO gains WaferId, LotId nullable string fields.
6. SqliteDefectStore.OpenAsync — REMOVE the explicit PRAGMA:
Before:
await conn.ExecuteAsync("PRAGMA foreign_keys = OFF");
After: (delete the line entirely; default Foreign Keys=True is correct)
Also keep `Cache=Shared` on the connection string — pooling is fine because
every store now expects FK enforcement.
7. ICassetteScheduler (src/InspectionPrototype.Application/Abstractions/ICassetteScheduler.cs):
See spec §"ICassetteScheduler + SimulatedCassetteScheduler" for the interface.
5 methods + 1 event + 1 property.
8. SimulatedCassetteScheduler (src/InspectionPrototype.Infrastructure/Simulator/SimulatedCassetteScheduler.cs):
- Constructor: IWorkflowService, IRunHistoryStore, ISimulatorProfileProvider,
IAppStateStore, ILogger<SimulatedCassetteScheduler>
- LoadCassetteAsync: builds CassetteState with TotalSlots slots, populates
each slot's WaferId via WaferId.Generate(lotId, i); updates AppState.Cassette.
Idempotent: if AppState.Cassette is already non-null, throws InvalidOperationException
(or logs Warning + returns; pick one and document).
- StartCassetteRunAsync: Task.Run(() => RunCassetteLoopAsync(ct)). Returns
Task.CompletedTask immediately (fire-and-forget); the loop runs on the
background.
- RunCassetteLoopAsync: see spec §"Cassette flow" pseudo-code. Per slot:
Loading → Task.Delay(profile.WaferLoadMs)
Aligning → Task.Delay(profile.WaferAlignMs)
Running → call _workflow.StartRunAsync(slot.WaferId, cassette.LotId);
await wait-for-terminal-state via polling AppState.WorkflowState
(or subscribe to StateChanged)
Unloading → Task.Delay(profile.WaferUnloadMs)
Advance to next slot
- Each phase transition fires CassetteStateChanged AND updates
AppState.Cassette via _store.Update.
- Single-wafer faults: log Warning + advance to next slot. Cassette keeps going.
- RequestCassetteStop: sets a flag; loop exits after current phase finishes.
- AbortCassetteAsync: cancels the loop's CTS immediately.
- UnloadCassetteAsync: sets AppState.Cassette = null; only allowed when
loop is not running (CurrentPhase ∈ {Idle, Complete}).
9. WorkflowService.StartRunAsync overload (Application/Services/WorkflowService.cs):
Add an overload:
public Task StartRunAsync(WaferId? waferId, LotId? lotId)
The existing parameterless StartRunAsync delegates to this overload with
nulls.
In the overload, AFTER existing guard checks, BEFORE starting the run loop:
var stubSummary = new RunSummary(
RunId: runId,
RecipeName: recipe.Name,
StartedAtUtc: startTime,
EndedAtUtc: DateTimeOffset.MinValue, // placeholder; updated at run end
TerminalStatus: RunTerminalStatus.Pending, // <-- new
DefectCount: 0,
MajorAlarms: [],
CompletedScanPoints: 0,
TotalScanPoints: recipe.ScanPoints.Count,
SimulatorProfileName: state.SelectedSimulatorProfile.Name,
DefectsMinor: 0,
DefectsMajor: 0,
DefectsCritical: 0,
WaferId: waferId?.Value,
LotId: lotId?.Value);
_ = SafeRunHistoryPersistAsync(() => _historyStore.SaveAsync(stubSummary));
Implement SafeRunHistoryPersistAsync as a private method mirroring
SafeAlarmHistoryAsync — try/await/catch Warning.
At run end (existing finally block), the existing _historyStore.SaveAsync(summary)
call replaces the stub row via INSERT OR REPLACE. The terminal RunSummary
carries WaferId and LotId from the constructor arguments.
10. AppState evolution (Application/State/AppState.cs):
Add at end of record:
CassetteState? Cassette = null
Update AppState.Initial accordingly.
11. SimulatorProfile fields (Application/State/SimulatorProfile.cs and
Infrastructure/Simulator/SimulatorProfilesOptions.cs):
Add three new fields:
int WaferLoadMs = 2000
int WaferAlignMs = 1500
int WaferUnloadMs = 1500
Update validators to reject values outside [100, 60000].
12. DI wiring (InfrastructureServiceCollectionExtensions):
Add right after SqliteDefectStore registration:
services.AddSingleton<ICassetteScheduler, SimulatedCassetteScheduler>();
13. Tests:
- MigrationRunnerM003M004Tests:
* AppliesM003OnVersion2Database: starting from M001+M002 state; assert
schema_version contains {1, 2, 3}; PRAGMA table_info(run_summaries)
returns wafer_id and lot_id columns.
* AppliesM004OnVersion3Database: assert {1, 2, 3, 4}.
* IsIdempotentOnVersion4Database: re-run; version count stays at {1..4}.
- SqliteDefectStoreFkIntegrationTests:
* DefectInsert_SucceedsAfterRunSummaryStubPersisted:
1. Persist stub run_summary (TerminalStatus = Pending).
2. Persist defect referencing the same run_id.
3. Assert no exception; SELECT verifies the row.
* DefectInsert_FailsWithFkError_WhenParentRowMissing:
1. Skip the stub persistence step.
2. Persist defect referencing a non-existent run_id.
3. Assert SqliteException with FK-error code (Code 19, ResultCode 787 NotNull or similar).
* NoOrphanDefectsAfterCassetteRun:
1. Run a small simulated cassette (3 wafers).
2. SELECT COUNT(*) FROM defects d LEFT JOIN run_summaries r
ON d.run_id = r.run_id WHERE r.run_id IS NULL.
3. Assert count = 0.
- WorkflowServiceStubRowTests:
* StartRunAsync_PersistsStubRow_WithPendingTerminalStatus:
arrange a recording IRunHistoryStore; call StartRunAsync; verify
SaveAsync was called once with TerminalStatus = Pending; verify
stub WaferId/LotId match the input args.
* RunComplete_UpdatesRowToTerminalStatus:
run a fast simulated run; verify two SaveAsync calls (stub + terminal);
terminal call has the actual TerminalStatus.
- SimulatedCassetteSchedulerTests:
* LoadCassette_PopulatesAllSlots: 25 slots, all WaferIds set per pattern.
* LoadCassette_RejectsIfAlreadyLoaded.
* StartCassetteRun_DrivesAllPhasesPerSlot: use very-small Wafer*Ms
(50ms each) and a 3-slot cassette; assert phase transitions
captured in CassetteStateChanged event log: Loading→Aligning→
Running→Unloading×3.
* SingleWaferFault_AdvancesToNextSlot: stub IWorkflowService that
returns a Faulted run on slot 1; assert cassette continues to
slot 2.
* StopRequest_CompletesCurrentPhaseAndHalts.
* AbortRequest_CancelsImmediately.
* UnloadCassette_ResetsAppState.
- SimulatorProfilesOptionsValidatorTests (extend existing):
* Reject WaferLoadMs < 100, > 60000; same for Align/Unload.
## Constraints
- Do NOT add UI changes — Pass 2 is the cassette UI.
- Do NOT skip the FK orphan-row integrity test — that's the verification of
the band-aid retirement.
- Do NOT change the existing direct-mode WorkflowService path beyond adding
the stub-row insert + the WaferId/LotId overload. Direct-mode runs
(legacy operator-driven) must still work without a cassette loaded.
- Do NOT introduce Cassette-related code in the Domain layer beyond the
pure value types (WaferId, LotId, etc.). Scheduler logic stays in
Application + Infrastructure.
- The stub-row's EndedAtUtc value can be DateTimeOffset.MinValue or
StartedAtUtc itself — pick the convention that doesn't violate any
validator. INSERT OR REPLACE at run end fixes the value.
- RunTerminalStatus.Pending is added as the FIRST enum member so
default(RunTerminalStatus) = Pending. This is a small but load-bearing
detail: if a stub row is queried before run-end, callers reading the
enum get a meaningful default.
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release --filter "Category!=Performance&Category!=Capture"
Manual smoke (no UI yet, but persistence + scheduler work):
- Delete inspection.db.
- Launch app; observe migrations M001+M002+M003+M004 applied in logs.
- Use a debugger or scripted call to ICassetteScheduler.LoadCassetteAsync +
StartCassetteRunAsync with a 3-slot cassette and small Wafer*Ms.
- Observe AppState.Cassette state transitions; verify defects table
contains rows for each wafer; confirm FK enforcement is live by
querying PRAGMA foreign_keys.
## Report format when finished
- files created / modified / deleted
- confirmation tests pass
- the FK orphan-row count from SqliteDefectStoreFkIntegrationTests' simulated
cassette run (should be 0)
- a single commit hash
- commit message: "feat(workflow): add cassette scheduler + stub-row pattern; retire FK band-aid (pass 1/3 of TASK-3.2)"Pass 2 — Cassette UI
You are implementing Pass 2 of TASK-3.2. Pass 1 (cassette scheduler +
stub-row pattern + FK retirement + AppState.Cassette) is merged. This pass
adds the CassetteStatusPanel WPF UserControl, MainViewModel projection,
and 4 new MainWindow buttons.
NO captures (Pass 3).
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-3.2-cassette-cadence.md (criterion 11)
- src/InspectionPrototype.Application/State/AppState.cs (Pass 1 — Cassette field)
- src/InspectionPrototype.Domain/Contracts/CassetteState.cs (Pass 1)
- src/InspectionPrototype.Presentation/ViewModels/MainViewModel.cs
- src/InspectionPrototype.Presentation/Controls/WaferMapView.xaml
(Pattern: similar UserControl from SLICE-3.1)
- src/InspectionPrototype.App/MainWindow.xaml (where the new buttons + panel slot in)
- tests/InspectionPrototype.Tests/MainWindowAutomationIdRegressionTests.cs
Pass 1 must be merged. Confirm: AppState.Cassette field exists,
SimulatedCassetteScheduler exists.
## Scope of this pass
Cassette UI panel + ViewModel projection + 4 new MainWindow buttons +
AutomationIds + tests. NO captures.
## Deliverables
1. CassetteSlotViewModel
(src/InspectionPrototype.Presentation/ViewModels/CassetteSlotViewModel.cs):
public sealed class CassetteSlotViewModel
{
public int SlotIndex { get; init; }
public string WaferIdText { get; init; }
public string DisplayState { get; init; } // "Empty" / "Pending" / "Done" / "Current"
public Brush StateBrush { get; init; } // gray / blue / green / yellow
}
2. MainViewModel updates:
- Add: public ObservableCollection<CassetteSlotViewModel> CassetteSlots { get; } = new();
- Add: public string? CassetteLotIdText { get; private set; }
- Add: public string CassetteCurrentSlotText { get; private set; } = "—";
- Add: public string CassetteCurrentPhaseText { get; private set; } = "Idle";
- Add: public bool IsCassetteLoaded { get; private set; }
- In Project(state):
if (state.Cassette is { } cassette) {
IsCassetteLoaded = true;
CassetteLotIdText = cassette.LotId.Value;
CassetteCurrentSlotText = $"{cassette.CurrentSlotIndex + 1} / {cassette.TotalSlots}";
CassetteCurrentPhaseText = cassette.CurrentPhase.ToString();
DiffCassetteSlots(cassette);
} else {
IsCassetteLoaded = false;
CassetteLotIdText = null;
CassetteSlots.Clear();
}
- DiffCassetteSlots: similar diff pattern as RecentDefects, prepend-by-index;
keep CassetteSlots size = cassette.TotalSlots; update DisplayState/StateBrush
for each slot based on whether SlotIndex < CurrentSlotIndex (Done),
== CurrentSlotIndex (Current), or > CurrentSlotIndex (Pending).
3. CassetteStatusPanel UserControl
(src/InspectionPrototype.Presentation/Controls/CassetteStatusPanel.xaml + .xaml.cs):
<UserControl
x:Class="InspectionPrototype.Presentation.Controls.CassetteStatusPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="280">
<StackPanel
AutomationProperties.AutomationId="CassetteStatusPanel"
Visibility="{Binding IsCassetteLoaded, Converter={StaticResource BoolToVisibility}}">
<TextBlock>Lot: <Run Text="{Binding CassetteLotIdText}" /></TextBlock>
<TextBlock>
Wafer:
<Run AutomationProperties.AutomationId="CassetteCurrentSlotText"
Text="{Binding CassetteCurrentSlotText}" />
</TextBlock>
<TextBlock>
Phase:
<Run AutomationProperties.AutomationId="CassetteCurrentPhaseText"
Text="{Binding CassetteCurrentPhaseText}" />
</TextBlock>
<ItemsControl ItemsSource="{Binding CassetteSlots}" Margin="0,10">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="5" /> <!-- 25 slots = 5 × 5 grid -->
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1" Margin="2"
Width="40" Height="30" Background="{Binding StateBrush}">
<TextBlock Text="{Binding SlotIndex}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</UserControl>
4. MainWindow.xaml integration:
- Add 4 new buttons (alongside existing buttons, in their natural place):
<Button AutomationProperties.AutomationId="LoadCassetteButton"
Content="Load Cassette"
Command="{Binding LoadCassetteCommand}" />
<Button AutomationProperties.AutomationId="StartCassetteRunButton"
Content="Start Cassette Run"
Command="{Binding StartCassetteRunCommand}" />
<Button AutomationProperties.AutomationId="StopCassetteButton"
Content="Stop Cassette"
Command="{Binding StopCassetteCommand}" />
<Button AutomationProperties.AutomationId="UnloadCassetteButton"
Content="Unload Cassette"
Command="{Binding UnloadCassetteCommand}" />
- Add the CassetteStatusPanel control:
<controls:CassetteStatusPanel />
placed near the WaferMapView panel.
5. MainViewModel commands:
- LoadCassetteCommand: calls _scheduler.LoadCassetteAsync(LotId.GenerateForToday())
- StartCassetteRunCommand: calls _scheduler.StartCassetteRunAsync()
- StopCassetteCommand: calls _scheduler.RequestCassetteStop()
- UnloadCassetteCommand: calls _scheduler.UnloadCassetteAsync()
- Each command's CanExecute reflects cassette state (Load only when no
cassette loaded; Start only when loaded and idle; Stop only when running;
Unload only when not running).
6. AutomationId regression test:
Add to MainWindowAutomationIdRegressionTests.cs:
"CassetteStatusPanel",
"CassetteCurrentSlotText",
"CassetteCurrentPhaseText",
"LoadCassetteButton",
"StartCassetteRunButton",
"StopCassetteButton",
"UnloadCassetteButton",
7. Tests under tests/InspectionPrototype.Tests/:
- MainViewModelCassetteProjectionTests:
* NoCassette_ProducesEmptyCollection_AndIsLoadedFalse.
* LoadedCassette_PopulatesSlotsAndProgress.
* SlotIndexAdvances_UpdatesCurrentSlotText.
* PhaseTransition_UpdatesCurrentPhaseText.
* UnloadedCassette_ClearsCollection.
- (Optional) acceptance test in InspectionPrototype.AcceptanceTests/:
* smoke FlaUI test that asserts CassetteStatusPanel is visible after
clicking LoadCassetteButton.
## Constraints
- Do NOT add value converters in XAML beyond a generic BoolToVisibility
(probably already exists in the project; reuse).
- Do NOT clear-and-rebuild CassetteSlots on every state change — diff
pattern preserves visual identity. With 25 slots, the perf impact is
small but the pattern matters.
- Do NOT change any non-UI Pass 1 code.
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release --filter "Category!=Performance&Category!=Capture"
Manual smoke:
- Launch app; observe LoadCassetteButton enabled, others disabled.
- Click Load Cassette; observe 25-slot grid appears; LotId populated.
- Click Start Cassette Run; observe slot grid colors transition (Pending →
Current → Done) as wafers progress.
- Click Stop Cassette mid-run; observe scheduler completes current phase
and halts.
- Click Unload Cassette; observe panel hides.
## Report format when finished
- files created / modified
- confirmation all tests pass
- a screenshot or description of the cassette panel mid-run
- a single commit hash
- commit message: "feat(ui): add cassette status panel + 4 cassette buttons (pass 2/3 of TASK-3.2)"Pass 3 — 25-wafer cassette capture under Soak8h + row block + Phase 2 trigger assessment + runbook §5.4
You are implementing Pass 3 of TASK-3.2, the final pass. Passes 1 and 2
are merged. This pass runs a 25-wafer cassette capture under Soak8h
profile, appends the slice-3-2-cassette-cadence row to phase-3-measurements.md,
writes runbook §5.4, and applies the SLICE-2.0 decision rubric.
NO code changes — Passes 1 and 2 own those.
## Authoritative references
Read these before making changes:
- docs/specs/SLICE-3.2-cassette-cadence.md (criteria 12, 13, 14, 15, 16)
- docs/runbook/capturing-measurements.md (§5.1–§5.3 — mirror for §5.4)
- docs/reviews/phase-3-measurements.md (slice-3-3 + slice-3-1 row formats)
- tools/Capture-Measurements.ps1
- tools/MeasurementExtraction.psm1
## Scope of this pass
Capture, two new MeasurementExtraction helpers, row block append, runbook
§5.4, Phase 2 trigger assessment, session-handoff updates. No code.
## Deliverables
1. tools/MeasurementExtraction.psm1:
Add and export two helpers:
- Get-WafersCompletedCount -DatabasePath $db -LotId $lotId:
SELECT COUNT(*) FROM run_summaries
WHERE lot_id = @lotId AND terminal_status != 'Pending'
Return integer count.
- Get-CassetteWallClockSeconds -DatabasePath $db -LotId $lotId:
SELECT
(julianday(MAX(ended_at_utc)) - julianday(MIN(started_at_utc))) * 86400.0
FROM run_summaries
WHERE lot_id = @lotId AND terminal_status != 'Pending'
Return seconds (real number).
Update ConvertTo-MeasurementRow to optionally accept -LotId; when set,
append two rows to the markdown:
| wafers.completed (count) | <Get-WafersCompletedCount> |
| cassette.wall-clock (s) | <Get-CassetteWallClockSeconds> |
2. tests/Tools/MeasurementExtraction.Tests.ps1:
Two new Pester Describe blocks with synthetic SQLite databases. Verify
correct count + wall-clock math; verify $null for missing lot_id.
3. Capture procedure:
$date = Get-Date -Format 'yyyy-MM-dd'
$dbPath = "$env:LOCALAPPDATA\LcnWaferInspection\inspection.db"
# Step 1: backup pre-existing DB
if (Test-Path $dbPath) { Move-Item $dbPath "$dbPath.preCapture-$date" }
# Step 2: launch app once to apply migrations M001+M002+M003+M004
# then close.
# Step 3: capture before-snapshot
Copy-Item $dbPath "$env:TEMP\inspection.db.before-$date"
# Step 4: launch app, switch to Soak8h profile, click:
# Connect → Home → Load Cassette → Start Cassette Run
# The 25-wafer cassette runs ~14-15 min under Soak8h.
# IMPORTANT: this is NOT a Capture-Measurements.ps1 invocation; the
# cassette scheduler doesn't go through MultiTagSoakFlaUi. Run dotnet-
# counters collect manually for the duration:
# Start-Process dotnet-counters -ArgumentList "collect --process-id $(Get-Process InspectionPrototype.App).Id --output docs/captures/slice-3-2-cassette-cadence-$date.csv --refresh-interval 1 --counters InspectionPrototype,System.Runtime --duration 00:20:00" -NoNewWindow
# Wait for cassette to complete (observe CassetteStatusPanel — phase = Complete);
# click Unload Cassette → Disconnect → close app.
# Step 5: take after-snapshot
Copy-Item $dbPath "$env:TEMP\inspection.db.after-$date"
# Step 6: extract the LotId from the database
$lotId = (sqlite3 $dbPath "SELECT lot_id FROM run_summaries WHERE lot_id IS NOT NULL ORDER BY started_at_utc DESC LIMIT 1;")
# Step 7: extract row block via PowerShell
Import-Module ./tools/MeasurementExtraction.psm1
ConvertTo-MeasurementRow `
-CsvPath "docs/captures/slice-3-2-cassette-cadence-$date.csv" `
-SliceTag slice-3-2-cassette-cadence `
-Scenario CassetteCadence `
-CommitHash $(git rev-parse --short HEAD) `
-Date $date `
-DatabaseBefore "$env:TEMP\inspection.db.before-$date" `
-DatabaseAfter $dbPath `
-LotId $lotId
Verify:
* exit code 0; CSV span ≥ 800 s (14 min minimum)
* Get-WafersCompletedCount returns 25
* Get-CassetteWallClockSeconds returns ≤ 1200 s (20 min ceiling)
* No SqliteException Error 19 entries in app log
* Reproducibility: first 26 metrics within ±15% of slice-3-1 baseline
(slightly looser bound to absorb cassette-mode overhead)
4. Append row block to docs/reviews/phase-3-measurements.md:
Standard 32-metric set (from slice-3-1 baseline) + 2 new cassette-specific
metrics. Notes section MUST include:
(a) Why slice-3-1 is the baseline (most recent Phase 3 row).
(b) Cassette criterion satisfaction: 25 wafers completed in <20 min wall-clock.
(c) FK band-aid retirement evidence: 0 SqliteException Error 19 entries
in the app log; PRAGMA foreign_keys reports 1 on a fresh connection;
0 orphan rows from SELECT COUNT(*) FROM defects d LEFT JOIN
run_summaries r ON d.run_id = r.run_id WHERE r.run_id IS NULL.
(d) Phase 2 trigger assessment per the SLICE-2.0 rubric. Cassette mode
drives ~25× more workflow state-transition rate than single-run
mode; expect store.update rate to climb. If alloc share crosses
10% or lock-wait p95 crosses 100 µs, open the relevant 2.x slice.
Otherwise document Phase 2 stays deferred against five measurement
points now.
(e) What surprised, if anything.
5. Add §5.4 to docs/runbook/capturing-measurements.md:
- Title: "### 5.4 Cassette cadence capture — SLICE-3.2, 25-wafer Soak8h"
- Procedure: manual cassette-driven (not the MultiTagSoakFlaUi orchestrator)
- Sanity checks: 25 wafers completed, wall-clock under 20 min, 0 FK errors.
- Phase 2 trigger assessment as the row's expected output.
6. Update CLAUDE.md "Current position":
- Phase 3: SLICE-3.2 Completed; FK band-aid retired; cassette mode
functional; Phase 2 trigger decision: <outcome>.
- Last completed action: TASK-3.2 Pass 3 — 25-wafer cassette capture;
wall-clock <X> s; <Phase 2 outcome>; commit <hash>.
- Next action: SLICE-3.4 (identity + audit) or SLICE-4.1 (real SDK swap)
depending on prioritization.
7. Append session-log entry to docs/reviews/roadmap-progress.md.
Mark SLICE-3.2 Completed in the Phase 3 progress table.
## Constraints
- Do NOT make any code changes.
- Do NOT skip the 25-wafer cassette criterion. If the cassette doesn't
complete in 20 min wall-clock or fewer than 25 wafers persist, the
slice's exit gate is unmet — investigate before proceeding to row edit.
- Do NOT pre-decide the Phase 2 trigger outcome.
## Verification before you report done
dotnet build --configuration Release
dotnet test --configuration Release --filter "Category!=Performance&Category!=Capture"
Plus:
- docs/captures/slice-3-2-cassette-cadence-<date>.csv committed
- docs/reviews/phase-3-measurements.md has the row with all 34 metrics
- SQL verification: 25 rows in run_summaries with the captured lot_id;
zero rows in any "orphan" check
- CLAUDE.md current-position reflects SLICE-3.2 closure
## Report format when finished
- files created / modified
- the captured row block (markdown table) included verbatim
- 25-wafer cassette wall-clock + Phase 2 trigger assessment outcome
- 0 / non-zero counts of FK errors and orphan rows
- a single commit hash
- commit message: "feat(measurements): SLICE-3.2 row + cassette cadence verified; FK band-aid retired (pass 3/3 of TASK-3.2)"Operator notes
- One pass per Copilot session. Same protocol as TASK-1.4 / 2.0 / 3.3 / 3.1.
- Pass 1's stub-row + FK retirement is the load-bearing architectural fix. The
RunTerminalStatus.Pendingenum value, the stub_historyStore.SaveAsyncat run start, and theSqliteDefectStore.OpenAsyncPRAGMA removal are coupled — all three must land in the same commit, otherwise FK violations re-appear. TheSqliteDefectStoreFkIntegrationTests.NoOrphanDefectsAfterCassetteRuntest is the regression gate; if it ever fails, the band-aid is back. - Pass 1's
RunTerminalStatus.Pendingplacement. Adding it as the FIRST enum value sodefault(RunTerminalStatus) = Pendingmatters because reducer code occasionally constructs partialRunSummaryrecords during transitions; defaulting toPendingavoids "default(enum) = Completed" bugs that would silently report stub rows as terminal. - Pass 2's MainViewModel cassette projection mirrors the SLICE-3.1 RecentDefects diff pattern. With 25 slots, the perf impact is small, but the pattern matters because it preserves visual identity for unchanged slots — a Critical-severity slot doesn't visually flicker as the cassette progresses.
- Pass 3's capture is manual, not orchestrated. The cassette scheduler isn't reachable via
MultiTagSoakFlaUi's scenario; capture runs by clicking the new buttons in the UI whiledotnet-counters collectruns alongside. Document this in §5.4 explicitly so future captures don't try to use the FlaUI orchestrator. - Phase 2 trigger watch is critical for this row. Cassette mode is the highest workflow-state-transition rate the prototype has yet measured (each wafer drives ~5 phase transitions plus ~10 store updates from the run loop, × 25 wafers = ~375 state transitions in ~14 min). If alloc share or lock-wait p95 ever crosses thresholds, this is the most likely capture to surface it. Apply the rubric mechanically.
- Update the index files only at the end of the phase, not per-slice. SLICE-3.4 (identity + audit) is the next slice; the index sweep happens after 3.4 lands.