# Walkthrough — from the device simulator to the HTML

A guided trail through v0.1: follow the data from the **device simulator** to the **HTML** you can open.
Everything here is **local — just `node`** (in-process device + in-memory event log). No MQTT or Postgres is
needed until the *integration* step at the end.

**The one input that drives everything:** the published **Workflow** at
`../architecture/structural-model/retailer-a/`. Nothing downstream hard-codes anything — it all reads from here.

All paths below are relative to `platform/`.

---

## Step 0 — the input: the published Workflow

`../architecture/structural-model/retailer-a/`
- `01-customer-solution.json` — tenant, **roles** (operator/manager/zonal/auditor/admin), masters.
- `02-blueprint.json` — the process **graph + rules** (the *authoring* layer; runtime doesn't read this directly).
- `03-workflow.json` — customer **bindings, slots, `encodingStandard`, roleBindings** (what runtime reads).
- `04-expected-shipment.json` — the **PO ▸ ASN ▸ HU** shipment (6 HUs × 50).

`engine/src/engine.js` → `loadConfig()` pulls these in as `cfg`.

> **Terminology:** runtime reads the **Workflow** (the published, customer-bound bundle). The **Blueprint** is the
> layer above — it *declares* the process; the Workflow *materialises* it. (In this sandbox the two are still separate
> files; a real publish merges them.)

---

## Step 1 — the device simulator (your starting point)

- `device-simulator/src/device-interface.js` — the swappable **Device seam** (`openSession · prepare · read · write · verify · state · events`).
- `device-simulator/src/simulator.js` — `SimulatedDevice`: scenario-driven (`happy/short/excess/invalid/writefail/notags/offline`), produces TIDs and runs its **own state machine** (`Idle → Preparing → Ready → Scanning → Encoding → Verifying → Complete`).

See it in isolation — reads + state machine:
```bash
cd platform/device-simulator
node --input-type=module -e "import {SimulatedDevice} from './src/simulator.js'; const d=new SimulatedDevice('happy',50); d.openSession({deviceId:'BX-TN-WH01-01'},[]); d.prepare(); const {rawTids}=d.read(); console.log('raw reads:',rawTids.length,' unique:',new Set(rawTids).size); console.log('device states:',d.events.map(e=>e.state).join(' → '));"
```

*(`pass-device.js` is the same seam, but fed by an MQTT message instead of a scenario — that's for the integration.)*

---

## Step 2 — the engine consumes the device (the heart)

`engine/src/engine.js` → `runProcessInstance(cfg, hu, device, log, encoder)` walks the Workflow graph:

```
SELECTED (device.prepare)  →  SCANNING (device.read → dedup + TID-master)  →  RECONCILING (count/tolerance guards)
  →  ENCODING (encoder.computeEpc + device.write)  →  VERIFYING (device.verify)  →  ACCEPTED (emit events)  →  COMPLETED
```

It emits a **canonical event** at each meaningful step. Run the harness to watch device → engine → events → stock
across every scenario:
```bash
cd ../engine && node src/run.js       # 27/27 — happy, short, excess, invalid, writefail, notags, offline, replay, 6-HU, concurrency
```

---

## Step 3 — the encoder (called by the engine at ENCODING)

`encoder/src/encoder.js` → `RealEncoder.computeEpc(ean, serial, standard)` → real **SGTIN-96** (i-TEK's lifted
encoder lib). The **engine computes** the EPC; the **device writes** it.
```bash
cd ../encoder
node --input-type=module -e "import {RealEncoder} from './src/encoder.js'; const e=new RealEncoder(); const epc=e.computeEpc('8901234567890','1','gs1-ean13'); console.log('EPC:',epc,'  decodes to EAN:',e.decode(epc).barcode);"
```

---

## Step 4 — the event log (where the engine writes truth)

`event-log/src/event-log.js` — `EventLog`: **append-only**, tenant-scoped, **idempotent** (a replay dedups). Every
step the engine takes becomes one immutable event here. *(The durable twin is `pg-event-log.js` → Postgres; same
interface, used in the integration.)*

---

## Step 5 — the projection (stock is *derived*, not stored)

`event-log/src/projections.js` → `rebuildStock(events)` folds the event stream into the **stock** read-model. This is
why every number on the HTML is always consistent with what actually happened — it's re-derived from the log.

---

## Step 6 — runtime resolution (for the operator UI)

`runtime/src/resolver.js` → `resolveRuntime(cfg, role)` turns the Workflow's roles into **navigation + scope +
permitted operations** (PRD §7). This is what makes the UI reshape per role (operator operates; auditor is read-only).
```bash
cd ../runtime
node --input-type=module -e "import {loadConfig} from '../engine/src/engine.js'; import {resolveRuntime} from './src/resolver.js'; const cfg=loadConfig(); for(const r of ['dc_operator','zonal_manager','auditor']){const rt=resolveRuntime(cfg,r); console.log(rt.roleName.padEnd(24), 'scope',rt.scope.padEnd(4), rt.canOperate?'operates':'read-only', '· nav:',rt.navigation.map(n=>n.key).join(','));}"
```

---

## Step 7 — the assembly: the snapshot builder ties 1→6 together

`runtime-ui/src/snapshot.js` runs Steps 1–6 for each HU (a realistic shift), builds the view **model** (queue, live
transaction, roles, exceptions), and injects it into the template.

---

## Step 8 — template → HTML → open it

```bash
cd ../runtime-ui && node src/snapshot.js     # → runtime-ui.html
open runtime-ui.html
```
`template.html` is the rendering; `runtime-ui.html` is the output you arrive at. Switch roles in the page to see the
runtime reshape.

---

## Two HTMLs, same chain

| Output | Command | What it shows |
|---|---|---|
| **Operator runtime** | `cd runtime-ui && node src/snapshot.js && open runtime-ui.html` | role switcher, inward queue, live transaction, device session/health |
| **Inventory dashboard** | `cd customer-view && node src/snapshot.js && open customer-view.html` | accepted cartons, inventory posted, exceptions (customer-facing) |

## The one-line mental model

```
Workflow config → device sim (reads) → engine (walks graph) → encoder (EPCs)
   → event log (truth) → projection (stock) → resolver (role/nav) → snapshot (model) → template → HTML
```

---

## Then: the integration (next)

The integration swaps **Step 1's in-process device** for the **MQTT-injecting** one — same chain, real plumbing:
```
device-cli (Tunnel sim, publishes MQTT) → EMQX (shared subscription) → engine consumer → PgEventLog (Postgres) → projections
```
Run the end-to-end acceptance (needs EMQX + `garuda_events` up — see `event-log/docker-compose.yml`):
```bash
cd platform/event-log && npm run db:up && npm run migrate
cd ../acceptance && node src/run.js          # 24/24 across all 22 AC-RT
```
Plan: `../architecture/change-spec/INTEGRATION-PLAN.md`. Next slice: shadow-parity vs `inward-scanned-worker` → cut.
