1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
//! Classification of flow nodes that the runner engine executes itself
//! ("builtins") rather than resolving to a pack component.
//!
//! This is the single source of truth shared by pack validation
//! (`validate::ComponentReferencesExistValidator`) and the packc resolve/build
//! pipeline, so they never drift. It MUST mirror the runner's `NodeKind`
//! dispatch (`greentic-runner-host` engine). A key the runner dispatches but
//! this list omits makes `resolve`/`build` demand a component that can never
//! exist, so the pack cannot be built at all.
//!
//! # Read the right runner list
//!
//! The runner keeps TWO lists and they are not the same. `NATIVE_OP_KEYS`
//! (`runner/flow_adapter.rs`) governs loading a RAW `.ygtc`; a BUILT pack
//! reaches the engine through the compiled manifest, where `HostNode::from`
//! applies its own `is_builtin` prefix allowlist (`runner/engine.rs`). Only the
//! second one describes what a pack this crate produces will actually do.
//! Reasoning from `NATIVE_OP_KEYS` alone yields a confidently wrong answer —
//! it lists keys the compiled path still cannot dispatch.
//!
//! What makes the difference is that `is_builtin` decides whether a dotted
//! `component.id` is kept intact or split into `<component>.<operation>`. A key
//! that is split never reaches its own match arm, and the resulting prefix
//! (`"state"`, `"approval"`) is by construction never a key in the pack's
//! component map. That is the bug `approval.` and `var.` were added to
//! `is_builtin` to fix.
//!
//! # `state.get` / `state.set` are deliberately NOT here
//!
//! The runner has a native arm for both (`NodeKind::BuiltinStateGet` /
//! `BuiltinStateSet`, run by `execute_state_get`/`execute_state_set`) and lists
//! both in `NATIVE_OP_KEYS`. They are still absent below, for two independent
//! reasons — either one alone is disqualifying.
//!
//! **`state.get` is a real pack component in this repo.** `examples/qa-demo`
//! resolves it from `repo://io.3bridges.components.state@1.0.0`: digest-pinned in
//! `pack.lock.json`, declared in `pack.yaml` as `required_capabilities:
//! [state:get]`, and materialized with a wasm artifact by
//! `packc/tests/run_examples_manifest.rs`. Listing it here would make
//! `collect_flow_component_ids` skip it and silently drop that component from the
//! built pack. This is the `mcp.exec` hazard landing on the op-key ITSELF rather
//! than on a `.suffix`, so no placement is safe — `state`, a `state.` prefix and
//! an exact `state.get` all swallow it. `state.set` shares that component family
//! and its `state:` capability namespace, so it stays out alongside `state.get`
//! rather than being split off on the accident that no `state.set` component
//! exists yet.
//!
//! **The runner cannot dispatch either from a built pack today.**
//! `BuiltinStateGet`/`BuiltinStateSet` are unit variants: they read key/value out
//! of the payload and take nothing from `component.operation`, so a packc-emitted
//! state node always carries `operation: None`. The engine's `is_builtin` has no
//! `state.` arm, so that id is dot-split into component `"state"` + operation
//! `"get"` and dispatch fails with `component 'state' not found in pack`. Adding
//! these keys here would trade a loud build failure for a loud dispatch failure
//! without making one flow work.
//!
//! Closing this needs a `state.` arm in the runner's `is_builtin` FIRST, and then
//! a ruling on a genuine cross-repo contradiction: greentic-designer's
//! `catalog.baseline.yaml` calls `state.get` native (`component_ref: null`) while
//! `examples/qa-demo` resolves it as a component. One of the two is wrong, and
//! whichever way it goes, qa-demo's `pack.yaml`, `pack.lock.json`, both resolve
//! sidecars and `run_examples_manifest.rs` move in the same change.
//!
//! `var.set` used to be listed as blocked on the runner. It was not, for the
//! reason in "Read the right runner list" above: the compiled path already
//! carries a `"var."` prefix in `is_builtin` and a `"var.set"` arm that writes
//! `state.vars`. It is in [`BUILTIN_EXACT_KINDS`] below.
//!
//! Whoever extends this list: read the runner's `is_builtin`, not just
//! `NATIVE_OP_KEYS`, and check every addition against the `mcp.exec` hazard
//! documented on [`BUILTIN_EXACT_KINDS`] before trusting the prefix rule.
use Node;
/// Builtin/dispatch flow-node kinds. Agentic and dispatch kinds appear in flows
/// as `dw.agent.<agent_id>`, `dw.agent_graph.<graph_id>`, etc., so callers must
/// match both the bare kind and the dotted `<kind>.<suffix>` form.
const BUILTIN_KINDS: & = &;
/// Builtin kinds that match ONLY on equality — never as a `<kind>.<suffix>`
/// prefix.
///
/// `mcp` is here rather than in [`BUILTIN_KINDS`] because the prefix rule would
/// also swallow **`mcp.exec`**, a real component shipped with its own wasm and
/// manifest in `examples/weather-demo` and `examples/adaptive-mcp-oauth-demo`.
/// Under the prefix rule `mcp.exec` would be silently reclassified as
/// engine-dispatched, skip resolution, and vanish from the built pack — a
/// working pack quietly losing a component, which is worse than the build
/// failure this entry exists to fix.
///
/// The runner dispatches the bare `mcp` op-key natively (`NATIVE_OP_KEYS` in
/// `runner/flow_adapter.rs`, executed by `runner/mcp_node.rs`), and
/// `greentic-flow` lowers such a node to `component.id == "mcp"` with no
/// operation. There is therefore no pack component to resolve, and demanding a
/// resolve/summary entry for one made every flow containing an MCP node
/// unbuildable.
///
/// `var.set` is here for the same reason and with the same hazard: the engine
/// classifies it in `is_builtin` (a `"var."` prefix) and executes it through a
/// `"var.set"` arm that writes `state.vars`, so it has no pack component
/// either — while a prefix entry would capture any future `var.set.*`
/// component. greentic-designer's Set Variable palette node emits exactly this
/// op-key, and every flow using it was unbuildable.
///
/// `telco-x.call` is the runner's remote-dispatch node for the `"telco-x"`
/// runtime (`NodeKind::TelcoXCall` → `execute_telco_x_call` →
/// `execute_remote_dispatch`), the same family as `sorla.call` / `operala.call` /
/// `agentic.call` in [`BUILTIN_KINDS`]. Unlike the state kinds it takes its
/// `target` FROM `component.operation`, so its nodes carry an operation, which is
/// what keeps the engine from dot-splitting the id — its match arm is reachable
/// from a compiled manifest even though `is_builtin` has no `telco-x.` prefix.
/// Nothing in greentic-pack, greentic-designer or greentic-runner ships a
/// component id starting with `telco-x`; the designer's own
/// `catalog.baseline.yaml` marks the capability `dispatch: true` with no
/// `component_ref` at all. It is exact-match anyway, because greentic-flow's
/// `classify_node_type` reads a 2-segment key as a builtin but a 3+-segment one
/// as an adapter component — so a `telco-x.call.*` id is precisely the shape a
/// prefix entry would misclassify and drop.
/// `flow.goto` is the runner's hand-over node (`NodeKind::FlowGoto` →
/// `NodeControl::Jump`), the counterpart to `flow.call` in [`BUILTIN_KINDS`]
/// above: a call awaits its target and fails on one that parks, a goto
/// transfers the turn to exactly such a target and does not come back. It is
/// engine-dispatched, so there is no pack component to resolve, and without an
/// entry here every flow containing one fails to build with `missing resolve
/// summary entries` — which is why greentic-designer currently ships the node
/// hidden from both its palette and its builder.
///
/// Exact-match rather than a [`BUILTIN_KINDS`] prefix entry, for the same
/// reason as `telco-x.call`: greentic-flow's `classify_node_type` reads a
/// 2-segment key as a builtin but a 3+-segment one as an adapter component, so
/// a `flow.goto.*` id is precisely the shape a prefix entry would misclassify
/// and silently drop from the built pack. The designer emits the bare key with
/// its target in a nested payload (`flow.goto: { flow_id, node?, input? }`),
/// never a dotted suffix, so equality is sufficient as well as safer.
///
/// Note the deliberate asymmetry with its own sibling `flow.call`, whose prefix
/// entry predates that hazard being understood, and is not a precedent to copy.
const BUILTIN_EXACT_KINDS: & = &;
/// Whether a component-id string names a runner builtin (engine-handled, with
/// no pack component to resolve). Accepts both the bare kind (`dw.agent`) and
/// the dotted form (`dw.agent.support`); `emit.*` is always builtin.
///
/// [`BUILTIN_EXACT_KINDS`] entries are matched by equality only — see that
/// constant for why `mcp` must not participate in the dotted-prefix rule.
/// The kind-bearing id of a flow node. A `dw.agent.<id>` (and other dispatch)
/// node compiles to the generic `component.exec` placeholder with the real kind
/// carried in `operation`, so the operation is authoritative when the component
/// id is empty/`component.exec`; otherwise the component id is.
/// Whether a flow node is a runner builtin (so it needs no resolve mapping,
/// summary entry, or manifest component reference).