# nr-flow-validator Declarative Node-RED flows with conftest/Rego validation. Generates `flows.json` from a YAML spec, validates against a Rego policy pack, deploys via the Node-RED HTTP API. ## Layout ``` infra/ # host stack config (compose, z2m, mosquitto, nodered settings) flows/.yaml # declarative spec scripts/flow2json.py # YAML → JSON generator policy/flow.rego # Rego policy pack (conftest) output/.json # generated flows.json ``` ## Quick start ```bash just new my-flow # copy template $EDITOR flows/my-flow.yaml just validate # generate + validate just deploy # push to Node-RED at 192.168.81.147:1880 ``` See `infra/README.md` for the full Docker stack and rebuild procedure. ## Generation invariants These are enforced by `policy/flow.rego` and burned in from real bugs found in deployment. The generator follows them by construction; the policy is the second line of defense. 1. **Flat structure only.** Node-RED 5.0 silently breaks MQTT broker connection when a tab has an internal `nodes: [...]` array. Always emit all nodes at top level with a `z` field referencing the tab id. Use `policy/flow.rego::deny_wrapped_tab` to reject wrapped structure. 2. **Every node needs `wires: []`.** Missing or non-array `wires` throws TypeError in `Flow.js::rewireNode`. `deny_missing_wires` enforces this. 3. **MQTT brokers must have `autoConnect: true`.** Without it the broker never reconnects after the first user disconnects. `deny_no_autoconnect`. 4. **`protocolVersion` must be `"3"`, `"4"`, or `"5"`.** `deny_invalid_protocol`. 5. **`ui-button` must have `icon: ""`.** Dashboard 2.0 throws TypeError on `prependIcon` otherwise. Generator sets this explicitly. 6. **Dashboard widgets need a `z` field** referencing their tab id, otherwise the SPA renders the group but the widgets never appear. Generator sets this. 7. **Wire references must resolve to known node ids.** `deny_wire_ref`. ## Validation rules summary | Rule | Catches | |---|---| | `deny_missing_type` | node without `type` | | `deny_missing_field` | required field missing for type | | `deny_broker_ref` | mqtt in/out references unknown broker | | `deny_group_ref` | ui-* references unknown group | | `deny_page_ref` | ui-group references unknown page | | `deny_wire_ref` | wire targets unknown node | | `deny_missing_flow` | wire references a node not in `z` of a tab | | `deny_isolated_node` | node with no wires in/out (config nodes exempted) | | `deny_broker_port` | mqtt-broker port outside 1-65535 | | `deny_wrapped_tab` | tab with internal `nodes` array (nodered 5.0 bug) | | `deny_missing_wires` | node without `wires` array | | `deny_no_autoconnect` | mqtt-broker without `autoConnect: true` | | `deny_invalid_protocol` | mqtt-broker protocolVersion not in 3/4/5 | | `deny_unused_broker` | broker defined but never referenced | ## Deployment target Node-RED on RPi 3B+ DietPi at `192.168.81.147:1880`, persisted via Docker bind mount to `/root/nodered`. The deploy target's `flows.json` is the source of truth in production; this generator is for declarative editing. ## Dashboard 2.0 SPA quirk Dashboard 2.0's `dist/index.html` ships with relative asset paths that break at `/dashboard/home`. `/data/start.sh` on the host patches in `` on container start. Without that patch the SPA loads but assets 404 and nothing renders.