K1: SubstrateHistory ring buffer per substrate

Each Substrate now retains an append-only history of state transitions
(both location and processing axes), the triggering event captured as
a std::variant<SubstrateEvent, SubstrateProcessingEvent>, the location
label at the time, and a steady_clock timestamp.

E90 §6.6 requires the equipment to be able to report a wafer's
processing history — typically queried via S6F11 batched reports or
SVID reads.  This commit lays the runtime substrate; wire query
plumbing is the natural follow-up.

set_history_limit(n) caps per-substrate retention (default 256, 0 =
unbounded).  Oldest entries are dropped when the cap is reached;
vector-erase is fine at this scale (typical wafer lifecycle is a few
dozen transitions).

Two new test cases cover the recording invariants (every fire results
in one history entry on the right axis) and history_limit eviction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 08:43:24 +02:00
parent 06f664dfab
commit a28a8b5982
2 changed files with 87 additions and 1 deletions
+36
View File
@@ -71,6 +71,42 @@ TEST_CASE("SubstrateStore: create + dedup + location update") {
CHECK(s.get("W-001")->fsm->location_state() == SubstrateState::AtWork);
}
TEST_CASE("SubstrateStore: history records every transition") {
SubstrateStore s;
s.create("W-H", "CAR-X", 3);
s.fire_location_event("W-H", SubstrateEvent::Acquire, "MOD-A");
s.fire_processing_event("W-H", SubstrateProcessingEvent::StartProcessing);
s.fire_processing_event("W-H", SubstrateProcessingEvent::EndProcessing);
s.fire_location_event("W-H", SubstrateEvent::Release, "DEST");
const auto* hist = s.history("W-H");
REQUIRE(hist != nullptr);
CHECK(hist->size() == 4);
// First entry is a location transition to AtWork.
CHECK((*hist)[0].location_to == SubstrateState::AtWork);
CHECK((*hist)[0].processing_to == SubstrateProcessingState::NoState);
CHECK((*hist)[0].location_label == "MOD-A");
// Third entry is a processing transition to Processed.
CHECK((*hist)[2].processing_to == SubstrateProcessingState::Processed);
CHECK((*hist)[2].location_to == SubstrateState::NoState);
}
TEST_CASE("SubstrateStore: history_limit caps retention") {
SubstrateStore s;
s.set_history_limit(3);
s.create("W-CAP");
// Generate more transitions than the cap.
s.fire_location_event("W-CAP", SubstrateEvent::Acquire);
s.fire_location_event("W-CAP", SubstrateEvent::Return);
s.fire_location_event("W-CAP", SubstrateEvent::Acquire);
s.fire_location_event("W-CAP", SubstrateEvent::Release);
const auto* hist = s.history("W-CAP");
REQUIRE(hist != nullptr);
CHECK(hist->size() == 3);
// Oldest entry (Acquire to AtWork) should be evicted; first remaining
// entry is the Return.
CHECK((*hist)[0].location_to == SubstrateState::AtSource);
}
TEST_CASE("SubstrateStore: change handlers observe both axes independently") {
SubstrateStore s;
std::vector<std::pair<std::string, SubstrateState>> loc_log;