feat(daemon): Phase D — GEM300 in-the-loop (jobs, recipes, EC changes)

Semantics settled and documented: v1 is observe-and-report. The engine keeps
acking S16/S3/S7/S2F15 from its FSM tables — exactly the behaviour both
reference implementations validated — while the tool observes lifecycle
events on the Subscribe stream and reports physical progress back. Gating
stays the documented v2 deferred-reply item.

Engine: two new store observers (HandlerSlot pattern) — RecipeStore fires
(ppid, body) after an add (S7F3 downloads), EquipmentConstantStore fires
(id, value) on ACCEPTED S2F15 writes only. Unit-tested.

Daemon: the service registers PJ/recipe/EC observers (io thread; add_
observers coexist with register_default_handlers' primaries) and fans the
new HostRequest variants out via push_request (fire-and-forget, no-
buffering contract). ProcessJob carries action (Start->START, Resume->
RESUME, Paused->PAUSE, Stopping->STOP, Aborting->ABORT) + recipe + material
bindings read store-side on the io thread. ReportProcessJob maps SETTING_UP
->SetupComplete, COMPLETE->ProcessComplete, ABORTED->AbortComplete via
read_sync; PROCESSING is informational; unknown job => INVALID_OBJECT,
table-rejected transition => CANNOT_DO_NOW. Carriers deferred (CarrierStore
has no observer machinery; ReportCarrier stays UNIMPLEMENTED) — roadmap.

Python client: on_process_job / on_recipe / on_constant_change decorators +
report_job(job_id, state); ProcessJob dataclass exported.

Tests: daemon suite 141 -> 175 assertions — the full in-process loop
(S16F11 create -> tool setup -> S16F5 PJSTART -> stream ProcessJob with
recipe+carriers -> ReportProcessJob(COMPLETE) -> FSM at ProcessComplete),
rejection paths, S7F3 -> ProcessProgram, S2F15 -> ConstantChange with the
configured name. Core 475/3097 (observer units). Live regression: daemon
interop 20 checks + pyclient 13 checks still green against the running
daemon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 23:53:45 +02:00
parent b30443089f
commit b1772cfefd
10 changed files with 333 additions and 10 deletions
+11 -1
View File
@@ -1,12 +1,14 @@
#pragma once
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "secsgem/gem/handler_slot.hpp"
#include "secsgem/secs2/item.hpp"
namespace secsgem::gem {
@@ -45,8 +47,15 @@ struct FormattedRecipe {
class RecipeStore {
public:
// Observe recipe arrivals (e.g. a host S7F3 download): fires after the
// store is updated, with (ppid, body). Multi-observer via HandlerSlot —
// the gRPC daemon forwards these onto its Subscribe stream.
using AddedHandler = std::function<void(const std::string&, const std::string&)>;
void add_added_handler(AddedHandler h) { on_added_.add(std::move(h)); }
void add(std::string ppid, std::string body) {
by_ppid_.insert_or_assign(std::move(ppid), std::move(body));
auto it = by_ppid_.insert_or_assign(std::move(ppid), std::move(body)).first;
if (on_added_) on_added_(it->first, it->second);
}
std::optional<std::string> get(const std::string& ppid) const {
auto it = by_ppid_.find(ppid);
@@ -95,6 +104,7 @@ class RecipeStore {
private:
std::map<std::string, std::string> by_ppid_;
HandlerSlot<const std::string&, const std::string&> on_added_;
std::map<std::string, FormattedRecipe> formatted_;
};