MES Module — Developer Reference
Manufacturing Execution System for Laraplate. Turns confirmed demand and item master data into executable production, tracks shop-floor progress, consumes materials, traces lots/serials, records quality and downtime, and exposes production KPIs.
Boundaries
- Depends on ERP (one-way: ERP knows nothing of MES). Physical FKs point at
ERP
companies,items,warehouses,sales_orders,sales_order_lines. - Every table is prefixed
mes_and centralised inMESTables. - Stock movements are defined by the MES
StockMovementRecordercontract and executed by the ERP adapterErpStockMovementRecorder. - Component holds go through the ERP
StockReservationServiceunder the opaque source aliasmes.production_order_material(see «Component reservation»). - Production order numbers are allocated by ERP
DocumentNumberAllocatorusingDocumentType::ProductionOrder.
Entities
| Area | Models |
|---|---|
| Work centers | WorkCenter, WorkCenterCalendar |
| BOM | Bom, BomLine (validity window; audited) |
| Routing | Routing, RoutingOperation (validity window; audited) |
| Production | ProductionOrder (immutable BOM/routing snapshots; audited), ProductionOrderOperation |
| Materials | MaterialConsumption |
| Traceability | LotNumber, SerialNumber, LotLineage |
| Quality | QualityPlan, QualityPlanCharacteristic (validity window), QualityCheck, QualityCheckMeasurement, NonConformance |
| Downtime | Downtime |
| Shifts | Shift, ShiftInstance, OperatorLog |
| Machine connectivity | MachineSource (linked to a service account), MachineProfile, MachineDevice, MachineSignal, UnmappedSignal, MachineMessage, MachineIncident, MachineAgentConfig, MachineAgentConfigReport |
Core flow
- Create (
ProductionOrderService::create) — allocates the number and freezes the effective BOM lines and routing operations into immutable JSON snapshots (bom_snapshot,routing_snapshot). Statusdraft. - Release (
release) — materialisesProductionOrderOperationrows from the routing snapshot. Statusreleased. Once the release is persisted,ProductionOrderReleasedreserves the order's BOM components in ERP stock (see «Component reservation»). - Execute operations (
ProductionOrderOperationService) —start/complete/skip; efficiency = standard / actual, clamped[0, 999.99]. Completing an operation logs the operator (OperatorLog), dispatchesBackflushMaterialsJob, and creates the in-processQualityCheckfrom any active plan (see «Quality plans»). - Backflush (
BackflushMaterialsJob, idempotent) — consumes snapshot BOM lines markedbackflushwhoserouting_operation_idmatches the operation (or the order's last operation when null — decision D5). Each line reads on-hand stock viaStockReaderand consumes the available quantity: the stockoutis posted for what exists, and any shortfall flagsmes_material_consumptions.stock_shortage(with a negativevariance) and emitsMaterialShortageDetected. Non-blocking and stock never goes negative. After the stock-out, the ERP reservation of the line is closed for the part that was held (see «Component reservation»). - Order state machine —
draft → released(release, emitsProductionOrderReleased)→ in_progress(the firstProductionOrderOperationService::start()on a released order, emitsProductionOrderStarted)→ completed(complete, emitsProductionOrderCompleted).cancelis allowed from draft, released and in progress: the operations that did not complete (planned or running) becomeskipped, andProductionOrderCancelledis emitted and releases the order's component reservations. Operations emitOperationStarted,OperationCompletedandOperationSkipped. Events carry ids only and are dispatched after the transition is persisted. - Complete order (
complete) — receives the finished goods into the order's warehouse (FinishedGoodsReceiptService, see below), refused (DomainException) while any operation isin_progress; sets produced quantity; generates the finishedLotNumberwhen the item is lot/serial-traced; creates the final-inspectionQualityCheckfrom any active plan.
Sales-order-driven creation
Confirming an ERP sales order dispatches Modules\ERP\Events\SalesOrderConfirmed
(fired on create-as-confirmed and on the draft → confirmed transition). MES
listens with the queued CreateProductionOrdersForSalesOrder, which delegates to
SalesOrderProductionPlanner. Per line the planner creates a draft production
order only when the line has an item with an active BOM; it plans the
outstanding quantity (qty_ordered − qty_delivered) and links the order back via
sales_order_id / sales_order_line_id. Planning is idempotent per line (an
existing order short-circuits), so replays and re-confirmations never duplicate.
The receiving warehouse comes from ProductionWarehouseResolver (config map →
company's sole warehouse → skip when ambiguous); planned dates come from
ProductionLeadTimeEstimator (routing standard minutes ÷ daily minutes, or the
default lead time when the item has no routing). Purchased/service lines, already
delivered lines and multi-warehouse-ambiguous lines are skipped with a reason
(ProductionPlanningSkipReason) rather than guessed.
Quality plans
A QualityPlan is a date-effective (version/valid_from/valid_to/is_active)
set of expected characteristics (QualityPlanCharacteristic: nominal,
lower_limit, upper_limit — the same shape as QualityCheckMeasurement) for an
item, optionally scoped to a routing_operation_id. When an operation completes,
QualityCheckPlanner resolves the plan for (item, routing_operation) and creates
a pending QualityCheck linked to it; when the order completes it resolves the plan
with routing_operation_id = null (final inspection). Creation is non-blocking (no
plan → no-op) and idempotent per (order, plan, operation). Operators later run the
existing execute action, which evaluates measurements and opens a non-conformance
on failure.
Finished goods receipt and valuation
Completing an order posts an inbound movement of quantity_produced for the finished item in
the order's warehouse, in the same transaction as the completion (a failing receipt rolls the
completion back; nothing is posted for a quantity of zero). The unit cost is the order's
material cost over the quantity produced. The material cost is read through the
ProductionCostReader contract (ErpProductionCostReader): the sum of quantity x unit cost
of the ERP stock-outs sourced to the order, valued by the ERP (FIFO layer or weighted
average). ErpStockMovementRecorder now passes the production order as the movement source,
so every MES movement is traceable to it, and StockMovementData carries an optional
unit_cost for inbound movements. Labour and machine time have no cost rate, so they add
nothing. A backflush still queued when the order completes is not part of the cost.
Stock shortage
StockReader (ERP-backed ErpStockReader over StockLevel, the warehouse-pinned
reservations and the company-wide null-warehouse holds) is the read side of the stock boundary. Backflush and manual consumption read availability and consume
what exists: the stock-out is posted for the available quantity
(quantity_consumed), the shortfall is recorded as a negative variance with
stock_shortage = true, and MaterialShortageDetected is emitted. This keeps the
stock ledger truthful (never negative, which the ERP recordOutbound rejects) and
turns a hard ERP exception into a structured, non-blocking early warning. The
queued NotifyMaterialShortage listener sends MaterialShortageNotification to
the recipients configured by role (mes.notifications.stock_shortage).
Component reservation
A released production order holds its BOM components in ERP stock, so a component promised
to one order is no longer available to another. MES calls the ERP StockReservationService
through ComponentReservationService; ERP only sees the opaque source alias
mes.production_order_material and never resolves a MES class.
- Reserve at release.
ProductionOrderService::release()dispatchesProductionOrderReleasedafter the release transaction commits, and the synchronousReserveComponentsForProductionOrdercallsreserveForOrder(). For every frozen snapshot line that has anitem_idand amaterial_line_id(whatever its consumption method) it computes the requirement as the per-unit quantity times the order quantity (quantity_produced, elsequantity_planned) and takes ahardreservation ofmin(requirement, company-wide available)pinned to the order's warehouse. It is best effort: a short component reserves what exists (nothing when none is left) and never blocks the release; there is no early shortage event, the existingMaterialShortageDetectedstill fires at backflush. A per-linereserve()error (a lost availability race, a lock timeout, or a validation failure) is caught and logged with context (company, order, item, line); it never propagates out of the release transition, and the other lines still reserve. - Release at cancel or completion.
ProductionOrderCancelledrunsReleaseComponentsForProductionOrderandProductionOrderCompletedrunsReleaseComponentsAfterCompletion; both release the live hold of every snapshot line (idempotent; consumed quantity is terminal). Releasing on completion is what keeps a completed order — which can no longer be cancelled — from leaking its backflush shortfall and its manual-consumption holds. - Consume at backflush.
BackflushMaterialsJobkeeps its path (post the stock-out, recordquantity_consumed/variance, flagstock_shortage, emitMaterialShortageDetected) and then, for the line, closesmin(quantity consumed, quantity still hard-reserved)throughconsumeForLine(). The consumable is sized as the reader's availability plus this order's own hard hold for the line:ErpStockReadersubtracts every warehouse-pinned hold, this order's included, so adding its own hold back lets the order consume the stock it reserved for itself while the ceiling stayson hand − other orders' holds. A company-wide null-warehouse (sales) hold is never the order's own, so it is not added back and stays subtracted: backflush can never consume stock a sales order reserved. Only the reserved part is closed: a line that never reserved is a no-op, and stock beyond the reservation is consumed physically as before. The reservation close is not in the same transaction as the stock-out. - Per-warehouse availability.
ErpStockReader::availableQuantity()is the on-handStockLevelof the(item, warehouse)minus the soft/hard reservations pinned to that same warehouse and the item's company-wide null-warehouse holds (sales reservations). A soft row counts by state alone, so an expired but not yet swept soft hold still counts until the ERP expiry sweep closes it. The null-warehouse subtraction is conservative: a company-wide hold is taken off every warehouse's availability (it may under-report when stock is spread across warehouses) so MES can never oversell stock another module reserved company-wide. material_line_idkeying. The reservationsource_idis the snapshot line'smaterial_line_id, stamped by theProductionOrdercreatedboot hook on every creation path (service, factory, import, direct create) asorder id * 1000 + line index. It is unique to one order and stable for the whole lifecycle (reserve, release, consume). The templatebom_line_idcannot be used: it is shared by every order built from the same BOM and would pool holds across them. A line that already carries amaterial_line_id(a test fixture) is left untouched; a line that still has none after stamping is skipped by reserve, release and consume.
Known limitations:
- The
* 1000scheme bounds a production order to fewer than 1000 component lines; thecreatingboot hook rejects a snapshot that reaches 1000 lines with aDomainException(before insert), so a larger BOM can never silently collide with the id range of the next order. - Availability is read two ways: the reserve clamp uses the company-wide ERP availability, while the warehouse-pinned hold is read per warehouse. The reader does subtract the company-wide null-warehouse (sales) holds, but conservatively (off every warehouse), so per-warehouse ATP stays a deferred non-goal.
- The backflush closes the line's reservation in a separate transaction from the stock-out movement, so a crash between the two can leave the stock posted while the hold is still open until the next release/cancel/completion of the order.
Services
BomExplosionService (multi-level explosion + active BOM), RoutingResolverService
(date-effective routing), ProductionOrderService, ProductionOrderOperationService,
LotTracingService (forward/backward genealogy), QualityCheckService
(limits → non-conformance), NonConformanceService (dispositions; rework spawns a
linked order), CapacityService (work-center load, available minutes, overload), OeeCalculatorService
(A×P×Q, clamped), DowntimeService, ShiftVerificationService,
SalesOrderProductionPlanner (auto-creation from confirmed sales orders, with
ProductionWarehouseResolver and ProductionLeadTimeEstimator),
QualityPlanResolver + QualityCheckPlanner (auto quality checks on completion),
ErpStockReader (StockReader read for shortage detection: on-hand minus warehouse-pinned reservations and company-wide null-warehouse sales holds),
ComponentReservationService (reserve, release and consume ERP reservations for a production order's BOM
components).
HTTP surface
No custom routes. Entities are reachable through Core's generic CRUD
(/app/crud/{verb}/mes/{entity}). Domain verbs use Core's domain-action
registry (POST /app/crud/{action}/mes/{entity}): production-orders
release/complete/cancel, operations start/complete/skip,
quality-checks execute, non-conformances resolve/close, downtimes close, work-centers open_downtime,
boms explode, lot-numbers forward_trace/backward_trace. Registered by
MesDomainActionRegistrar; authorized by MesModelPolicy against seeded
{connection}.{table}.{action} permissions (seeded by MESDatabaseSeeder
from MESPermissions). A downtime is opened with the open_downtime action on its
work center (payload cause, optional production_order_operation_id and notes), which
refuses a second open downtime on the same work center and emits DowntimeOpened
(close emits DowntimeClosed); the generic insert still exists for back-filling
history and bypasses those rules. Aggregate reads have no routes: the ProductionDashboardWidget
shows four counts (open orders, running operations, completed orders, open
non-conformances) cached for 60 seconds. OEE and capacity are materialised
(decision D10): mes:kpis:materialize (hourly, --day=Y-m-d to backfill) queues one
MaterializeWorkCenterKpisJob per active work center, which stores a WorkCenterKpis
(availability, performance, quality, OEE, capacity load, available minutes, overload flag)
in the cache through WorkCenterKpiMaterializer and WorkCenterKpiStore. Reads never
recompute: a day not materialised yet has no figure. The work-center list shows the
OEE of today.
Capacity and downtime
CapacityService::getCapacityLoad() sums the standard minutes (setup + cycle ×
planned quantity) of the operations planned on a work center in a window. An
operation with planned dates counts in proportion to its overlap with the window; one
without dates counts whole when its order overlaps it.
availableMinutes() is the working minutes of the work center calendar in the window
(WorkCalendar: WorkCenterCalendar slots by day_of_week, several per day; a work
center with no slot works 08:00 to 16:00 every day) less every downtime overlapping it (DowntimeService::outOfServiceMinutesWithin():
planned maintenance included, because the work center is out of service either way;
each downtime is clipped to the window and an open one runs until now), never negative.
OEE availability keeps excluding planned maintenance
(unplannedMinutesWithin()) and clips downtimes to the window the same way.
checkOverload(work_center, from, to, ?available) compares the load with the
available minutes, or with an explicit budget when one is passed.
Planning is forward and infinite-capacity. scheduleOperations(order) (called by
ProductionOrderService::release()) writes planned_start_at / planned_end_at on each
planned operation: it starts when the previous one ends (a parallel one with it), at the
first working instant of its own work center, and lasts its standard minutes of working
time; it never looks at what else is planned on that work center.
estimateCompletionDate() plans the operations still to do from now (or from the planned
start while that is ahead) and returns the end of the last one; with nothing left it
returns the last actual end, or the order's planned_end_at when it has no operation.
rescheduleOperation(operation, work_center, ?planned_start_at) moves the operation and,
given a date, plans it again from there on the new calendar. Starting an operation on a work center whose materialised
KPIs flag an overload emits CapacityOverloadDetected (non-blocking, nothing is
recomputed live), notified to the roles in mes.notifications.capacity_overload.
Machine connectivity
User guide and protocol: docs/MACHINE_CONNECTIVITY.md. Step 1 of the machine data acquisition design:
the foundation, with no consumer of the machine data yet.
- Ingress.
POST api/v1/insert/mes/machine-messages(mes.api.machine-messages.insert, no alias for the former path): Core'sAuthenticateApiRequestauthenticates the bearer token of the source's service account (401missing, invalid, expired or revoked;403outside the token'sallowed_cidrs), thenResolveMachineSource:{model},{operation}checks theapipermission (insertonmes_machine_messages;401for the anonymous user,403otherwise), resolves the source throughMachineSourceResolver(403when the account speaks for no source) and refuses an inactive one (403), then themes-machine-ingestthrottle (mes.machine.rate_limit_per_minute, per service account), thenMachineDataIngestController(403not an http canonical source,413body or sample limits,422MachineEnvelopeValidator) andMachineMessageInbox::accept(), which stores the raw message throughIdempotentWriter(insert-or-ignore on five drivers, a unique-violation fallback on Oracle), recordsseq_gapandclock_skewincidents and queuesProcessMachineMessageJob. A refusal that names a source is oneauth_failureincident per source every five minutes, with the detail reasonmissing_permissionorinactive_source; a caller with no source is only logged. - Switch, limits and the agent role. The routes sit behind Core's
expose_apiswitch (403while it is off, the default). Core'sapilimiter counts per token once authenticated (core.api.rate_limit_per_minute). Sources are service accounts (users.is_service_account) linked bymes_machine_sources.user_id(nullable, unique, counting soft-deleted sources; only service accounts qualify).MESDatabaseSeedercreates theapirolemes-machine-agent(MACHINE_AGENT_ROLE; the seederdependsOn()PermissionRefreshSeeder) withinsertonmes_machine_messages,selectonmes_machine_agent_configsandinsertonmes_machine_agent_config_reports, granted without detaching, plus an ACL on the configurations'selectthat narrows the role to its own active source (RelationFilteronsource,user_id = @user.idandis_active = true; an installation seeded before the active-source condition keeps its stored ACL, which the seeder never overwrites).MachineMessage,MachineAgentConfigandMachineAgentConfigReportimplementRestrictsCrudWrites: Core's generic CRUD refuses their writes, so the permissions open the dedicated routes alone.bootstrap/app.phpcounts named limiters in Redis (throttleWithRedis()) except under the test runner. - Job.
ProcessMachineMessageJob(queuemes.machine.queue,WithoutOverlappingkeyed by source, 3 tries) owns retries, status and themessage_failedincident; the pipeline isMachineMessageProcessor:NormalizerRegistry(canonical,mapped_json),SignalResolver(one cached map per source, forgotten byMachineConfigurationObserver),OperationAttributor(explicit reference, else the single operation running at the sample time;prepare()loads the candidates of a message in one query),UnmappedSignalRecorder(batched; counts once per message and never on a reprocess), and the typed eventsMachineStateObserved,PartsCounted,ProbeMeasured,ProcessValuesSampled. - Tables. Configuration tables (
mes_machine_sources/profiles/devices/signals) extend Core's model with soft delete; pipeline tables (mes_machine_messages,mes_unmapped_signals,mes_machine_incidents) are plain Eloquent models. Messages areMassPrunable(processed ones pastinbox_retention_days). - Operations.
MachineMessageReprocessor(per message, per source range),MachineWatchdogandmes:machine-watchdog(every minute:device_silentincidents),MachineIncidentRecorderand theNotifyMachineIncidentlistener (mes.notifications.machine_incident),MachineProfileService(apply, import, export;MachineSignal::rulesForRole()validates a signal's config by role),UnmappedSignalMapper,MachineSourceResolver(the source of a service account, only users withis_service_account; soft-deleted sources never resolve). - Domain actions (
MESPermissions):MachineMessagereprocess;MachineSourcereprocess_range;MachineDeviceapply_profile;MachineProfileexport. Tokens are issued on the service account's user page (Core user resource, API tokens, withallowed_cidrs), never on the source. - Edge agent backend (plan
2026-10-15-mes-edge-agent-backend). Losses: the optionallosses[]of the envelope (prioritycritical/bulk,from_ts,to_ts,samples>= 1,reasonbuffer_full/rejected_invalid; schema andMachineEnvelopeValidator, lists enforced) reachesMachineMessageProcessor::recordLosses(), which records onedata_droppedincident per marker (detail{message_id, priority, from_ts, to_ts, samples, reason},occurred_at=to_ts) unless an incident with the same detail exists, so a reprocess, a duplicate or a second run adds none; the(source_id, type, occurred_at)index serves that lookup. Configuration:mes_machine_agent_configs(unique(source_id, version),documentjson cast to an object tree) andmes_machine_agent_config_reports;mes_machine_sources.active_config_id(FK added by the configurations migration) andlast_config_version.MachineAgentConfigService:publish(MachineSource, stdClass|string $document, ?User)validates structure (laraplate-edge-config/1, D3: top-level keys, integerversion,inputs/outputs/pluginsas arrays of objects with a stringid; copy inresources/edge/laraplate-edge-config-1.schema.json; plugin fields are not checked, but the agent requiressha256andmanifest_sha256per plugin and runs plugins only from its bootstrapplugins_dir), numbers the version from the locked source row, writes it into the document and activates it;activate()of a non-active version publishes it again as a new version (versions only grow, D2);recordReport()stores the report and opensconfig_rejected({version, outcome, errors}) forrejectedandrolled_back(D4). Routes:GET api/v1/detail/mes/machine-agent-configs(mes.api.machine-agent-configs.detail,MachineAgentConfigController:200 {version, document},304when?version=equals the active version,404none active,422bad version) andPOST api/v1/insert/mes/machine-agent-config-reports(mes.api.machine-agent-config-reports.insert,MachineAgentConfigReportController:201,422with nothing stored for a version not published for the caller's source), both behindResolveMachineSourceand themes-machine-agent-configlimiter (mes.machine.config_rate_limit_per_minute/MES_MACHINE_CONFIG_RATE_LIMIT_PER_MINUTE, per service account, shared by the two routes).MachineSource::delete()runs in one transaction and a hard delete clearsactive_config_idfirst. Backoffice: the source page has the relation managersAgentConfigsRelationManager(publish with a JSON editor, activate, view document; gated on theupdatepolicy of the source) andAgentConfigReportsRelationManager(read-only). Documents hold onlysecret://references. - MQTT (step 2).
php artisan mes:machine-bridge(MachineBridgeCommandoverMachineBridge): connects throughMachineMessageSubscriber(PhpMqttSubscriber, php-mqtt/client, MQTT 3.1.1, QoS 1, persistent session undermes.machine.mqtt.client_id), subscribes toMqttMessageRouter::subscriptions()(the effective topics of the activemqttsources, re-read every 60 s), hands each message toMqttIngest(router, canonical envelope validation,MqttPayloadEnvelopewrapper forsparkplug_b, thenMachineMessageInbox::accept()), writes the heartbeatmes:machine:bridge-heartbeatevery 10 s, backs off 1, 2, 4 ... 60 s onMqttConnectionLost, and stops onstop()(SIGTERM and SIGINT in the command) after the message it holds. A message that cannot be routed is dropped (log once a minute; an invalid canonical envelope opens amessage_failedincident).MachineWatchdogopensbridge_downper active mqtt source when the heartbeat is older than 60 s or absent. - Sparkplug B.
SparkplugBNormalizer(sparkplug_b) overSparkplugPayloadDecoderandProtobufReader(a wire-format reader; no protobuf library; fixtures encoded byprotocfrom the Tahu schema),SparkplugTopic, andSparkplugAliasStore(mes_sparkplug_aliases: alias to metric name per source and device, replaced at each birth, read by data). Devices are{node}or{node}/{device}; control and null metrics are skipped;source_seqis null (the Sparkplugseqwraps at 255). - Backoffice (group "Machine connectivity"): sources, devices with a signals relation manager, profiles (import and export), unmapped signals (map in place), the message inbox (reprocess), incidents.
- States and downtimes (step 3).
MachineStateRecorder(listener ofMachineStateObserved) callsMachineStateIntervals::observe()/tagAlarm()(idempotent history, split on late samples, merge of equal neighbours;MachineTimekeeps milliseconds) andMachineDowntimeDeriver::sync()(downtime when the state is inWorkCenter::isDowntimeState()and the stop is longer thanmicro_stop_threshold_seconds; times only are updated, cause and notes kept).DowntimeService::openFromMachine()/closeFromMachine()write throughDowntime::writingAsMachine();DowntimeCauseResolvermaps state and alarm code to a cause (defaultUnclassified).MachineConnectivity::isConnected()(active device of an active source with a state signal) gates manual downtimes and the ISO 22400 branch ofOeeCalculatorService::availability().mes:machine-open-stops(every minute) opens the downtime of stops still open;MachineWatchdogrecords and closes the syntheticOfflineinterval.WorkCenterKpis::$incomplete_dataand themes:kpi:v2:cache key. User guide:docs/MACHINE_CONNECTIVITY.md. - Counts (step 4).
PartsCountRecorder(listener ofPartsCounted) writesmes_machine_countsrows (MachineCount, delta in the column of the signal's role,raw_value, unique(signal_id, ts)) throughIdempotentWriter, withCounterDelta::between()(cumulative/delta mode, rollover, reset) and recomputes the row after a late insert.OperationCountTally::refresh()sums rows intomachine_good_quantityandmachine_scrap_quantityand firesOperationTargetReachedonce (target_reached_at).ProductionOrderOperationService::complete()prefillsdeclared_*;OperationQuantityDeclarer::declare()is the audited writer (mes_operation_quantity_audits);MachineCountAssigner::assign()attributes unattributed counts by time range.OeeCalculatorService::performance()andquality()use the counts when the window has any (ideal cycle of the operation, or60 / capacity_per_hourfor unattributed counts); cache keymes:kpi:v3:. User guide:docs/MACHINE_CONNECTIVITY.md. - Probe measurements (step 5).
ProbeMeasurementRecorder(listener ofProbeMeasured) takes a sample to the check of its operation whose plan holds the signal's characteristic (QualityCheckService::record(), thenisComplete()andresolve()when pending), or tomes_machine_unattributed_measurements(UnattributedMeasurement); an out-of-limit value dispatchesOutOfToleranceMeasuredonce. Idempotency by an existence check underlockForUpdateon the signal row (no unique index on measurements).QualityCheckPlanner::createCheck()callsUnattributedMeasurementAttacher::attachFor();UnattributedMeasurementAssigner::assign()is the manual path (Filament "Unattributed measurements").QualityCheckService::execute()=record()+resolve(). User guide:docs/MACHINE_CONNECTIVITY.md. - Process values (step 6).
ProcessValueRecorder(listener ofProcessValuesSampled) hands process-value samples (numeric, not older than the raw retention) to theProcessValueStorebound bymes.machine.process_store;RelationalProcessValueStorewritesmes_process_samplesin batches throughIdempotentWriter::insertMany(), marks minutes inmes_process_dirty_buckets(upsert, so a re-mark changesmarked_at),rollup()rebuilds1maggregates from raw and1hfrom the minutes (bad samples excluded, never from pruned raw),prune()/pruneAggregates()apply the retentions.mes:machine-rollup(every minute) andmes:machine-prune-process-values(daily).ProcessSummarizer::summarize()upsertsmes_operation_process_summaries(only with a count not below the stored one) from the unique queuedSummarizeOperationProcessValuesJob, dispatched bySummarizeOperationProcessValuesonOperationCompletedand by the recorder for completed operations when new samples were stored;LotTracingService::processSummaries(). User guide:docs/MACHINE_CONNECTIVITY.md. Everything in the design is built.
Backoffice
The Filament resources are the superadmin backoffice. Work centers edit their
weekly calendar and BOMs their lines inline (relationship repeaters; line edits
are versioned). Production orders are list and edit only, with read-only
relation managers for operations, material consumptions, quality checks and
lots, and the edit page carries Release, Complete (produced quantity, optional lot
code) and Cancel header actions. They call ProductionOrderService and are shown by
MesModelPolicy (called directly: the gate would let a superadmin past the state
guard); a refused transition is a notification. Day-to-day execution still belongs to
the application built on the services and domain actions.
Configuration
mes.queue.connection/mes.queue.name— queue for backflush and PO jobs.mes.lots.number_format— lot code tokens{YEAR}{MONTH}{DAY}{SEQ}.mes.production.default_warehouse—[company_id => warehouse_id]map for sales-order-driven PO creation (falls back to the company's sole warehouse).mes.production.daily_minutes(480) /mes.production.default_lead_time_days(5) — routing-based lead-time estimation and its no-routing fallback. Settings of moduleMES(groupmes), seeded byMESDatabaseSeederand overlaid onto config; an operator's value survives a re-seed. Read as at least1, so a stored0behaves as1.mes.notifications.stock_shortage.channels/.recipients.roles— channels (defaultdatabase) and recipient roles for the shortage notification.mes.machine.queue(mes-machine),.max_samples(5000),.max_body_kb(1024),.clock_skew_seconds(30),.inbox_retention_days(7),.rate_limit_per_minute(600) — machine connectivity; envMES_MACHINE_*.mes.notifications.machine_incident— channels and roles for incidents.
Locked decisions
See docs/superpowers/specs/2026-07-09-mes-module-decisions-design.md (D1–D11):
complete scope, ERP-based numbering, dual sales-order link, operation-scoped
backflush with last-operation fallback, non-blocking shift warning, snapshot
immutability, DIFF audit on ProductionOrder/Bom/Routing and on their lines and operations (BomLine, RoutingOperation; per-table setting versioning.strategy.{table}, seeded to DIFF; lines and operations are validated on every write and deleted for good), materialised KPIs (decided, not implemented yet),
and no lock by use: a BOM or routing stays editable because every order runs from its own snapshot.
Releases
Releases are cut by the maintainers with private tooling; do not bump versions or edit CHANGELOG.md by hand.