Repository navigation
Releases: factorysemantics/factorysemantics-mes
Release list
v0.3.1
0.3.1
Reference. What this release does, what it does not, and what is simulated.
Released: 2026-10-15. Previous: 0.3.0, on PyPI 2026-10-12.
What 0.3.1 is
Four pull requests on top of 0.3.0,
#170 to
#174, every
one about the first hour on somebody else's PC. 0.3.0 proved the plant starts
from pipx install on Windows; this release is what happens when it does not.
The first run on a Windows PC recovers itself
Three things went wrong in the first twenty minutes for the first person to
install 0.3.0 on Windows. All three are fixed:
py -m fsmes demo bottling # when `fsmes` is "not recognized"
py -m fsmes demo bottling status # when nothing answers on 8010
py -m fsmes demo bottling stop # when the plant is half deadpy -m fsmesworks.pip installputsfsmes.exein a Scripts folder a
default Python install does not put on PATH. Every command works this way now,
and a plant started this way starts its child processes this way, so PATH
never has to be fixed first.statusnames what died. A plant records what each of its five processes
is for, sostatussays which are alive and which is gone by role — the
dashboard and API, the OPC server, the reader, the simulated floor, the log
sink — prints the path of the gone one's log, and ends with the command to
type next. A healthy plant prints its dashboard URL.stopclears a half-dead plant, with no flag. When one process dies and
the rest keep running, nothing answers and nothing can be started over the
top.stopends the survivors and says what it found; a start on top of a
silent plant is refused in the same words instead of failing at bind five
times.
One page for that first hour:
run it on your own box.
A simulated plant shaped like your own
0.3.0 could write a file describing your plant's shape and do nothing with it:
fsmes skills unpack plant-from-your-mes .\assistant # offline, out of the wheel
# the three commands in assistant\SKILL.md, by hand on the MES box -> plant-shape.toml
fsmes demo from-shape .\plant-shape.toml --name acmeYour machines, routings, stop mix, order sizes and shifts, in six seconds, with
the one number the file cannot carry asked for on the way (the space between
each pair of machines). fsmes shape plant writes the pack without starting it.
One folder any assistant can walk you through
fsmes-assistant.zip is attached to this release, and
fsmes skills unpack assistant ./assistant writes the same folder out of an
installation with the network off. It is plain markdown you hand to whatever AI
assistant you already use — in a chat window, with no access to your machine.
Four stages: install until fsmes demo bottling answers; a plant shaped like
yours; this one beside your own MES in shadow mode, ending at the scorecard; and
an honest page on why "can I switch my old MES off?" is still answered no. It
never asks you for plant data: you run the commands and paste what was
printed.
fsmes doctor: one file you can paste to anybody
fsmes doctor --out doctor.md: one command, one Markdown file with the
version, the OS, how it was installed, every plant in this home with each process alive or gone, the ports and whether they
answer, and the last 40 lines of every process log with every value replaced by
its shape — a tag becomes AAAAAA_AAA_AAA, an order AA-AAAA-#, a measurement
<float>. For a plant in shadow mode a second half says how long it has been
watching, how much of that window it saw, the kept scorecards as one row each,
and what is not built toward a real switch-over. The file is read back by the
shape file's own leak check and is not written at all if any value got
through. It starts nothing, stops nothing, and nothing sends it anywhere:
one file to paste when something is wrong.
What 0.3.1 does not do
- It has not run a real plant. Not one, still. Every number in this
repository comes from a simulated line and says so where it appears. - No real SQL Server has run the shape queries. That path is proven on this
project's own fixtures and on Windows in CI, nowhere else. - Shadow mode has never run beside a real MES, so the doctor file's second
half has only ever described simulated plants. - A real switch-over is not built — the old MES off, this one booking for
real, writing to PLCs, carrying the old history. The folder says so. - You still need Python 3.12 or newer, the real obstacle on a locked-down
plant PC. Nothing here removes it. - Everything 0.3.0 does not do, it still does not do: 0.3.0.
What is simulated
Everything that produces a number, as in 0.3.0: a deterministic line simulator
in src/fsmes/sim/, replayed over a real OPC UA server. A plant from your shape
file is shaped like yours; none of its readings are yours.
Upgrading from 0.3.0
pipx upgrade factorysemantics-mes # or pip install --upgrade
fsmes --version # 0.3.1No migrations. Nothing here changes the database schema, so a plant that
runs on 0.3.0 runs on 0.3.1 with no init-db step; d4e8a1c93b70 is still
head. The procedure, and 0.2.0's, is on
upgrading between versions.
Every change in this release: CHANGELOG.
v0.3.0
0.3.0
Reference. What this release does, what it does not, and what is simulated.
Released: 2026-10-13. Previous: 0.2.0, on PyPI 2026-09-17.
What 0.3.0 is
A hundred and seventeen pull requests,
#38 to
#163.
The reason to install it rather than read about it is the first paragraph.
A plant on your machine, in two commands
pipx install factorysemantics-mes
fsmes demo bottlingThose two lines, on Linux, macOS or Windows, bring up the six-station ACME
bottling line: an eight-hour shift replayed over a real OPC UA server, a
simulated floor booking lots and naming its stops, and the operator screens
reading it. It prints where the plant is — http://127.0.0.1:8010/dashboard —
and an account to sign in with. fsmes demo bottling stop puts it down;
fsmes demo machining is a second plant on 8020.
0.2.0 could not do this. The plant packs were in the repository and not in the
wheel, so fsmes demo bottling from a PyPI install had no plant to run. They
are in the package now, byte for byte the ones the lab runs, and the wheel grew
3 % for it. First start copies the pack out of the package and generates the
shift from its seed — about 8 seconds on Linux, about 21 on Windows on
GitHub's runners — and after that it starts straight away. Everything it writes
goes to one per-user directory (%LOCALAPPDATA%\fsmes on Windows,
~/.local/share/fsmes on Linux, ~/Library/Application Support/fsmes on
macOS, or wherever FSMES_HOME says); nothing under the installed package is
written to.
A CI job does exactly this on Ubuntu and on Windows on every pull request —
installs the built wheel into an empty directory, starts the plant, signs in,
checks the specifications are there, the OPC port answers, the floor is
watching and a lot has been counted, then stops it and checks both ports came
back. It is the first job in this project's history that starts a plant on
Windows, and it found three faults nothing else could: the Windows console's
code page mangling what the CLI printed, in two places.
You need Python 3.12 or newer on the machine. On a locked-down plant PC
that is the real obstacle, and nothing in this release removes it.
Quality you can click
The SPC tab draws the charts the plant's own specifications ask for: an
individuals-and-moving-range chart where a characteristic is measured one piece
at a time, an X̄/R chart where it is measured n at a time, and a p chart
where an inspector counts a lot rather than measuring it. Western Electric
rules 1 to 5 are drawn and recorded — one row per firing, with the window it
judged — and which of them raise a hold is the plant's choice: rules 1 to 4 do
by default and the moving-range rule does not, because a plant that has run a
year without a moving-range chart should not wake up to a queue of holds about
readings it already lived through. A p chart draws rule 1.
Click a point and a panel opens beside the chart with the reading, the rules
that fired on it, the gauge that took it, the machine's state and tags at that
moment, and the rest of the line. Three more things on the floor screens became
clickable in this release, each opening the records behind what you clicked
rather than a new screen:
- a point on a trend graph — the tag's reading, the machine, and what else
was happening; - a block on a machine's state timeline — what the stop was, how long, why
and who said so, the job that was on either side of it, and whether a
maintenance order covers it (planned work is not a breakdown); - a lot on a p chart — how many pieces were looked at and how many were
sent back, by defect.
A non-conformance is now worked rather than merely closed: open → under
review → dispositioned → closed, where the disposition is use_as_is,
rework, scrap or return and requires a reason. "Closed" never said what
happened to the material, which is the question a non-conformance exists to
answer.
Maintenance that reaches the right person
A maintenance order says which trade it needs, how much it matters, whether the
machine has to be stopped and when the job may be done. A dispatcher reads the
plant's own rules, the crew's skills and the roster, and gives the work to
somebody who can do it; a supervisor's screen shows what is assigned, what is
waiting and why. An SPC signal can raise a maintenance order, and a stop
covered by one reads as planned work on the timeline rather than as a
breakdown.
A chat on the AI tab that explains the chart
Ask the AI tab why a point is out of control and it answers from the plant's
own records, drawing the chart it is talking about. It needs
ANTHROPIC_API_KEY — without one the tab says so and the chat is off; the
rest of the product is unaffected. It costs about $0.12 a question at
current prices, and the plant keeps a row per turn, with the tool calls, the
cost and the outcome, so what the AI did is a record like anything else.
The separate floor assistant — the one that walks an operator through a
screen — runs on a local model through Ollama. Without Ollama on the machine,
that assistant is off and says so. Neither is required to run a plant.
Every floor row knows its shift
A production booking, a state interval, a check and a non-conformance now each
say which shift they fell in, and /analysis/* can be asked for a shift
instead of a number of hours — so how did my shift go is answerable. Three
rules decide which shift a minute belongs to, and an instant no pattern covers
is not attributed rather than filed under the nearest shift.
The lab plays a whole shift, with a chain in it
The simulated bottling line replays an eight-hour shift rather than an hour,
and the shift has a four-link chain of cause in it that the product has to be
able to follow, from production through maintenance and a tag to quality: a
changeover on the filler, a chiller maintenance order raised and never started,
the chiller's outlet temperature climbing all afternoon with the product
reaching the filler walking 8.4 → 6.4 °C behind it, and — half an hour later —
fill-height samples a millimetre and a half low with low_fill going from the
second rarest defect to the commonest in the lots counted in that window. No
limit on the filler is broken at any point: 6.4 °C is an ordinary number for a
cold drink. It is scripted on purpose — that is what makes it a test — and the
MES is not told the answer.
The full record is
CHANGELOG 0.3.0.
What 0.3.0 does not do
- It has not run a real plant. Not one, still. Every number in this
repository comes from a simulated line and says so where it appears. - No plant PC in a real plant has run it. Windows is proven on GitHub's
runner, which has no antivirus, no domain policy, no proxy and no firewall
prompt. A real plant PC may add all four, and the first one to do so will
find something. - The chat needs a key, and it is not free. No
ANTHROPIC_API_KEY, no
chat on the AI tab. No Ollama, no floor assistant. About $0.12 a question is
measured on this project's own usage, not promised. - The AI answers from the records, and the records are simulated. It has
never been asked a question about a real plant's data. - No SAP, Odoo, Oracle or NetSuite connector exists, and ERPNext Job Card
confirmations are still not built. Order-level round trip against a real
ERPNext v15.120.0 in CI is what exists. - No MQTT broker has been on the other end of either direction, so
Node-RED, United Manufacturing Hub and Grafana are untested for the same
reason. - PLC write-back is still a guarded recommendation queue, not a control
loop, and that is deliberate. - The upgrade gate proves one plant's shape, on SQLite. Nobody has
upgraded a populated PostgreSQL plant. Take the backup. See
upgrading.
What is simulated
Everything that produces a number. The plant behind every screenshot, scored
run and CI check is a deterministic line simulator in src/fsmes/sim/,
replayed over a real OPC UA server. The bottling plant's eight-hour shift, its
fill-height samples, its chiller and its chain of cause are all scripted, and
the script's answer key is in labs/multiplant/bottling/. The
largest plant is an 83-station cutlery works that serialises every piece, and
it is the public demo.
Every chart in the product states its coverage, its totals and, where it is
drawing simulated data, that it is simulated. A KPI that cannot be computed
honestly comes back unknown with the reason attached — including, new in this
release, performance and OEE on a machine whose counted work will not fit
inside its run time, which used to print a figure above 100 %.
The ERPNext in CI is real software in a container, not a simulation, but it is a
clean instance seeded by the job — not anybody's books.
Compatibility is the claims table: every row
carries the date it was last checked and what was run on that date, and rows
nothing has ever run against say never.
Upgrading from 0.2.0
fsmes backup --out /mnt/nas/fsmes
# stop the agent and the API
pip install --upgrade factorysemantics-mes
fsmes db-status --pack packs/yourplant
fsmes init-db --pack packs/yourplant
fsmes db-status --pack packs/yourplant
# start them againFifteen migrations, c8b1e40d7a92 to d4e8a1c93b70: twelve new tables and a
set of nullable columns. Two of them write to rows that already exist — the
shift backfill, and a 30-day warning window on every gauge — and both are
described, with what they assume, in
[upgrading betw...
v0.2.0
Thirty pull requests, #8 to #37, since 0.1.2 on 2026-09-08. All thirty were
opened by the maintainer, @kalwei; there are no
outside contributors to thank yet. Within each section the entries run in the
order a plant person would read them: the ERP and its connectors first, then
the pieces a first real plant needs, then plant packs and the fleet, then
packaging and CI. Read the Honesty section before upgrading a plant anyone
reads numbers off — it holds every change to what a number, a file name or a
setting means, each with its migration line.
Added
-
The outbox is a domain event log. It carried ERP confirmations and
nothing else, because the ERP sync read every pending row as one. It now
selects the kinds its contract can parse, which leaves room for the plant
events no ERP asked for: equipment state changes (the state entered,
the reason, the state left and how long that had been open) and order
holds and resumes, with the reason a hold always carries. Each is
written in the same transaction as the fact it describes, so an event
cannot exist without its fact or a fact without its event. The
unified-namespace publisher picks them up with no change: a machine going
down reaches
umh/v1/<enterprise>/<site>/…/<machine>/_mes/equipment_state_change, and
a hold reaches the site.MES_OUTBOX_DOMAIN_EVENTS=falsekeeps the log to
what the ERP is owed. See the unified namespace. (#15) -
fsmes erp setupandfsmes erp check. The ERPNext connector needs
five custom fields on Work Order (custom_mes_synced,
custom_mes_good_qty,custom_mes_scrap_qty,custom_mes_over_qty,
custom_mes_lot). Until now the only thing that created them was a demo
seeding script inlabs/, which is not in the wheel — so nobody who
installed the package could create them at all. The definitions moved into
the package,fsmes erp setupcreates any that are missing and is safe to
run twice, andfsmes erp checksays in words whether the URL, the
credentials, all five fields and the company are each in order, exiting
non-zero if the connector would not work.fsmes run-erp-syncruns the field check when it
starts and says so on the console; it still starts, because an ERP that is
briefly unreachable is not a reason to refuse to run. Thelabs/seeder
now uses the same definitions.
See the ERPNext connector. (#16) -
The ERPNext connector is tested against a real ERPNext. A new
ERPNext (live)job stands up ERPNext v15.120.0 in containers pinned to
image digests (labs/erpnext/), seeds the plant, and runs the whole round
trip: a Work Order submitted in ERPNext, pulled by the connector,
acknowledged, confirmed, and then checked by reading ERPNext's own
documents back — the custom fields,produced_qty, one submitted
Manufacture stock entry, the comment. A retried confirmation is asserted to
leave exactly one stock entry. The old live test fetched orders, asserted a
list, and was deselected by CI's own settings, so it had never run. The job
is not on every pull request: it takes about three and a half minutes and
runs only when the connector, its tests or its fixture change. See
the ERPNext connector. (#17) -
A connector contract, so the next ERP is not the first one all over
again. The ERP port had three methods — fetch, acknowledge, confirm —
and they said nothing about the three things that actually bit the
ERPNext connector: what has to exist on the far side, how a write is
proved to have landed, and how a person checks it before trusting it.
Each was fixed in ERPNext-specific code, so Odoo or SAP or Oracle would
have rediscovered all three. The port now carriesrequirements()
(what this connector needs on the ERP side, as data a person can act on),
setup()(create or verify it, idempotently) andcheck()
(connectivity, credentials and requirements in plain language, non-zero
when something is wrong). All three have defaults, so a transport that
needs nothing — and a connector written against the older three-method
port — keeps working.fsmes.integrations.erp.conformanceships
inside the package: eight obligations any connector can be run against
without vendoring this project's tests, each of them something a
connector got wrong once. Every adapter that ships passes it.
Writing an ERP connector is the page
for the person who would write the next one, and decision record
0020
says what this project requires before it calls one supported. Odoo, SAP
and Oracle are still not written; the contract and the suite exist, the
connectors do not. (#18) -
fsmes erp requirements. What the configured connector needs on the
ERP side, and which of it the MES can create itself — the list to send
whoever administers the ERP, who is usually not the person running the
MES. (#18) -
A unified-namespace publisher.
fsmes uns publishrelays the MES's
own event stream — the transactional outbox the ERP connector already
delivers from — to an MQTT broker as JSON, under an ISA-95 topic tree
read out of the equipment model (umh/v1/<enterprise>/<site>/<area>/<line>/<machine>/_mes/<kind>).
Broker, credentials, prefix and the enterprise and site names are
settings; every rung between them comes from the plant's own equipment
codes, at whatever depth it modelled them. Delivery is at-least-once with
the outbox's retry, backoff and dead-letter behaviour, and every payload
carries the event id a consumer dedupes on. The MQTT client is the new
[mqtt]extra, so the core install does not grow a dependency;
MES_UNS_MODE=logprints the whole namespace without a broker.
fsmes uns topicsandfsmes uns queueshow the tree and the backlog.
Off by default. See the unified namespace. (#11) -
Shadow mode:
MES_SHADOW=true. One setting that lets this MES watch a
real plant and change nothing in it. It reads the OPC UA tags and books
production exactly as it would in charge; every path by which it could
reach past its own database is shut. No setpoint reaches a machine (the
order-code write-back included, which was never approval-gated); no live
ERP is contacted; nothing is published to a broker; the ERP file adapter
reads its inbox and moves nothing, because that folder may be the
incumbent's; the optional cloud model is refused, so the plant's numbers
stay on the box;fsmes demorefuses to run a fake plant beside a real
one.MES_ERP_MODEmay only beofforfileandMES_UNS_MODEonly
offorlog— anything else refuses to start, in one sentence naming
the variable. A mode you never set is settled rather than refused.Every outbound path in the package is listed in one place,
fsmes/shadow.py, with what shadow mode does to each and why; a test
walks each closed path and holds the refusal, and a second test scans the
source for outbound primitives so a new path cannot be added without the
register hearing about it. It shows on every screen as a bar that cannot
be dismissed, infsmes info, atGET /healthandGET /shadow, in the
MCP server's description and per plant inlist_plants(), and in the
audit trail asshadow.on/shadow.offat start-up. There is no runtime
toggle: leaving shadow mode is a restart.
See running beside an existing MES. (#21) -
A second front door: events other systems tell this MES. The OPC agent
is how the MES sees a plant; it is not how it learns why a machine stopped,
what an inspector measured, or how many units somebody counted by hand.
Those are typed into whatever system the plant already has, and until now
none of it could reach the MES.fsmes.integrations.inbound.contractis
three typed shapes —DowntimeLabel,QualityResult,
ManualCount— each carrying who supplied it (source, a free name
such asreplay:incumbent-mes, plus asource_kindcategory), the
supplier's own id (external_key), and when the supplier recorded it
(recorded_at).fsmes.services.inboundwrites them through the services
that already own the rules, deduplicating on
(source, kind, external_key), so the same file delivered twice changes
nothing. It matters most for a shadow run: technicians label stops in the
incumbent, and a shadow that never hears those shows an unlabelled stop
where the incumbent shows a reason code, which reads as a data problem in
the shadow when it is a plumbing problem. (#22) -
The first inbound driver: files in a folder.
fsmes inbound watch
reads CSV — or the same rows as JSON — from one inbox per event type,
through a column mapping that is your configuration and not code. No
input file is ever deleted: it moves toprocessed/, or torejected/
when nothing could be taken from it, and a rejects report beside it states
the file's totals and the first reason for each row refused.fsmes inbound checksays whether the mapping and the folders are usable before
any file exists. Feeding the MES what people typed
elsewhere is the page. A SQL poller ...
v0.1.2
Fixed
pipx install factorysemantics-mes && fsmes demodid not run: the wheel
carried noconfig/tag_map.jsonorconfig/line_layout.json, so the
simulator had no line to run and nothing was ever booked. The four
default config files now ship as package data, and the settings fall
back to them when the relative path is absent (a path you set yourself is
never replaced). The release workflow refuses a wheel without them.- The release workflow's SBOM step asked for an image tag that does not
exist (#5); the docs workflow serialises pushes togh-pages(#5).
FactorySemantics MES 0.1.1
Fixed
- The container image did not build: the Dockerfile copied
pyproject.toml
withoutLICENSEandNOTICE, and the build backend refuses metadata
without the licence file. The 0.1.0 release reached PyPI but no image and
no GitHub Release; this version carries the same code with the image built. - The Scorecard workflow pinned an action version whose image had moved
registries (#3). - The cutlery walkthrough installer failed lint (#3).
0.1.0, same day: published to PyPI only; its release run failed in the image job before the GitHub Release step. 0.1.1 is the same code with the image built. The 0.1.0 notes:
The first public version. The code was developed privately from 2026-08-28
to 2026-09-07 in 34 pull requests; docs/history/private-era.md
lists them. Everything below is what a stranger gets on day one, and every
number in it comes from a simulated plant.
Added
- Kernel: master data, routings, work orders, dispatch, execution with lot
genealogy, an append-only audit trail, users and roles with capabilities. - OPC UA connectivity: tag maps from an engineering worksheet,
opc-browse
andopc-verify, a replay server for simulated lines, triggers as data, a
write-back recommendation queue with three guards, and certificates of
analysis at the end of the line. - Downtime and OEE with an explicit unknown segment; shift calendars.
- Quality: inspections, holds, non-conformances, SPC with capability withheld
when the data cannot support it, a gauge register with calibration. - Maintenance-lite, finite-capacity scheduling with promised dates,
serialisation and genealogy at ten million pieces a day (the cutlery lab). - ERP: a typed per-operation contract with an outbox, a REST adapter, a
B2MML-flavoured file adapter, and an ERPNext connector registered as a
module (fsmes.modulesentry pointerpnext). - The agent surface: an MCP server with 85 tools that go through the HTTP API
as theAGENTrole, dry-run on every write,on_behalf_ofand idempotency
keys; the floor assistant in the operator UI; recorded walkthroughs; agent
evals that ask whether an agent given only the tools can answer what the
plant knows. - Operations: plants from a registry (
fsmes plant), scored runs
(fsmes score) against the simulator's scripted truth, sweeps, a Docker
image and Compose file, systemd units for test and promoted environments. - Documentation site (MkDocs), decision records, security policy, code of
conduct, governance.
Honesty
- Counters book production by delta only; a counter falling toward zero is a
reset and books nothing. OEE reports unknown rather than zero when the
MES was not watching. Unlabelled downtime is reported as unlabelled. These
are the house rules and they are tested.