Skip to content

feat: let a producer say how its graph reads, without teaching the platform - #37

Merged
endrix merged 1 commit into
mainfrom
feat/producer-declared-render-hooks
Sep 4, 2026
Merged

endrix merged 1 commit into
mainfrom
feat/producer-declared-render-hooks

Conversation

@endrix

@endrix endrix commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Four hooks a dataflow producer needs and could not have. Each follows the pattern external-node-by-meta established: the producer states a fact, the platform derives something selectable from it, and the naming happens in the product's own stylesheet. No new product word enters core, and the gates stay green because there is nothing product-shaped to find.

The kind hook was already here and only half wired

<kind>-node was emitted inside the external branch, so it worked for exactly the case the first product needed and silently did nothing for any other. A producer introducing a kind of its own got a node the platform renders happily and its own stylesheet cannot select, with nothing anywhere to say why. Nothing about the hook ever depended on externality. It is hoisted, so every node gets it.

This sits alongside the node-families mechanism from #36 rather than competing with it: families are a product's declared vocabulary, this is a fallback selector derived from whatever kind arrived.

Port roles

control had a class from before roles were general, and the property panel and port views read it, so it stays. Every role now also gets port-role-<role>.

This matters more than styling. Roles are the axis a reader wants to switch off: wiring present on every single element carries no information until the moment it does, and without a hook there is nothing to hang that switch on.

Edges gain role, width and undirected

A dataflow network carries several kinds of connection at once — one that carries tokens, one that carries a value, one that carries the thing clocking all of it — and drawn identically they can only be told apart by tracing them by hand. undirected lets a producer ask for the no-arrow edge type that already existed for structural wiring, for a connection with genuinely no direction to draw.

Feedback, which has a real design question in it

The derived answer is a fact about the drawing: which edges point backwards in the order the layout will use. That is the honest answer when nobody knows better, and it stays the default.

But a producer often does know better, because in its own terms a loop has a designated closing element and the shape cannot show which one it is. Several edges of one cycle are equally good candidates by shape alone, and the greedy sequencing picks among them by degree. So meta.feedback anywhere in a document switches that whole graph to the producer's answer.

All-or-nothing, deliberately. The test that pins it names the forward edge of a two-node loop — a choice the derivation would never make, which is what proves the declaration is honoured rather than merely agreeing. Merging the two sets would mark one cycle twice, once where the producer said and once where the heuristic guessed, and a reader has no way to tell which mark carries meaning.

Compatibility

Everything except the hoisted kind class is inert unless a producer sets one of the new keys, so a document that declares nothing behaves exactly as before. The derived-class helper checks its input against a class-name pattern, because the value is interpolated into a class attribute and a producer is not necessarily careful; there is a test with a space and a quote in a kind.

Verification

  • 12 new tests in producer-declared-render-hooks.test.ts
  • Full platform suite passes, 4/4 neutrality gates pass, build succeeds

Still outstanding from the same list

Both client-side and larger: the hide-by-role toggle, and edge labels, which the edge view has no notion of today. The first consumer of this reached the same conclusion about roles from the other direction and filters them producer-side instead, which also removes the ports rather than leaving dangling pins.

…atform

Four hooks a dataflow producer needs and could not have. Each follows the
pattern `external-node-by-meta` established: the producer states a fact, the
platform derives something selectable from it, and the naming happens in the
product's own stylesheet. No new word enters core; the gates stay green because
there is nothing product-shaped to find.

The kind hook was already here and was only half wired. `<kind>-node` was
emitted inside the external branch, so it worked for exactly the case the first
product needed and silently did nothing for any other: a producer introducing a
kind of its own got a node the platform renders happily and its own stylesheet
cannot select, with nothing anywhere to say why. Nothing about the hook ever
depended on externality. It is hoisted, so every node gets it.

Port roles get the same treatment. `control` had a class from before roles were
general, and the property panel and port views read it, so it stays; every role
now also gets `port-role-<role>`. This matters more than styling: roles are the
axis a reader wants to switch off. Wiring that is present on every single
element carries no information until the moment it does, and without a hook
there is nothing to hang that switch on.

Edges gain `role` and `width`. A dataflow network carries several kinds of
connection at once — one that carries tokens, one that carries a value, one
that carries the thing clocking all of it — and drawn identically they can only
be told apart by tracing them by hand. `undirected` lets a producer ask for the
no-arrow edge type that already existed for structural wiring, for a connection
with genuinely no direction to draw.

Feedback is the one with a real design question in it, so it is worth being
explicit about why it works this way. The derived answer is a fact about the
DRAWING: which edges point backwards in the order the layout will use. That is
the honest answer when nobody knows better, and it stays the default. But a
producer often does know better, because in its own terms a loop has a
designated closing element and the shape cannot show which one it is — several
edges of one cycle are equally good candidates by shape alone, and the greedy
sequencing picks among them by degree. So `meta.feedback` anywhere in a
document switches that whole graph to the producer's answer.

All-or-nothing, deliberately, and the test that pins it names the FORWARD edge
of a two-node loop — a choice the derivation would never make, which is what
proves the declaration is honoured rather than merely agreeing. Merging the two
sets would mark one cycle twice, once where the producer said and once where
the heuristic guessed, and a reader has no way to tell which mark carries
meaning. A document that declares nothing behaves exactly as before.

The derived-class helper checks its input against a class-name pattern, because
the value is interpolated into a class attribute and a producer is not
necessarily careful. There is a test with a space and a quote in a kind.

Still outstanding from the same list, both client-side and larger: the
hide-by-role toggle in the palette header, and edge labels, which the edge view
has no notion of today.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant