flow: structured dtypes, and the widgets that read them

A series, record or list message declares its shape instead of riding
DType.JSON, so a widget binds a shape rather than some JSON and a wrong
binding is refused before anything runs. A list declares its item type,
which is what keeps list[float] expressible for a pipeline.

On top of that: an agenda over a list, a notification over a record, and
a dashboard alert channel that publishes engine faults as one — so a
panel can show what went wrong without a flow wiring it by hand.

Also: only None means a node published nothing, a falsy value of the
wrong shape is now the named error it always should have been; and the
gauge's readout says its size is viewBox geometry rather than type scale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 15:06:45 +02:00
co-authored by Claude Opus 5
parent 18837e8880
commit 413501c6ce
16 changed files with 751 additions and 43 deletions
+61
View File
@@ -187,3 +187,64 @@ def test_publishing_nothing_does_nothing():
pipeline.publish({})
assert pipeline.values() == {}
def query_chart(**config) -> dict:
return {
"source": "query",
"request": "heating.chart_req",
"request_dtype": "record",
"message": "heating.chart_series",
"dtype": "series",
"refresh_s": 30,
**config,
}
def test_a_widget_refuses_a_dtype_it_cannot_carry():
WidgetDef(id="s", type="switch", config={"message": "a.b", "dtype": "bool"})
# A document written before the editor recorded types binds anything.
WidgetDef(id="s", type="switch", config={"message": "a.b"})
with pytest.raises(ValueError):
WidgetDef(id="s", type="switch", config={"message": "a.b", "dtype": "float"})
def test_the_structured_widgets_bind_their_shapes():
WidgetDef(id="a", type="agenda", config={"message": "a.b", "dtype": "list"})
WidgetDef(id="n", type="notification", config={"message": "a.b", "dtype": "record"})
with pytest.raises(ValueError):
WidgetDef(id="a", type="agenda", config={"message": "a.b", "dtype": "json"})
with pytest.raises(ValueError):
WidgetDef(
id="n", type="notification", config={"message": "a.b", "dtype": "list"}
)
def test_a_querying_chart_asks_with_a_record_and_draws_a_series():
widget = WidgetDef(id="c", type="chart", config=query_chart())
# It publishes its request and reads the answer, so the canvas draws both.
assert widget.target == "heating.chart_req"
assert widget.messages == ["heating.chart_series"]
with pytest.raises(ValueError):
WidgetDef(id="c", type="chart", config=query_chart(dtype="float"))
with pytest.raises(ValueError):
WidgetDef(id="c", type="chart", config=query_chart(request_dtype="json"))
def test_a_querying_chart_keeps_no_ring():
"""The answer carries its own past; a ring would store it twice."""
widget = WidgetDef(
id="c", type="chart", config=query_chart(history={"points": 900})
)
assert widget.history_points == 0
def test_a_dashboard_is_cut_into_a_sane_number_of_columns():
for columns in (0, 49):
with pytest.raises(ValueError):
DashboardDef(name="house", columns=columns)