behavior-contracts 0.8.5 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,25 +20,71 @@ function rustErr(code, msgExpr) {
20
20
  * fail closed with ZERO bc-runtime import. Codes match run_behavior verbatim (byte-equal). `code()` /
21
21
  * `failure_code()` expose the stable code without a bc-runtime type. */
22
22
  function emitBehaviorErrorType() {
23
- return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
23
+ return `// ErrorKindwhat went wrong (the closed set of scp-error.md). A concrete enum: the covered plane
24
+ // carries no strings-as-tags and no dynamic kind lookup.
25
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
26
+ pub enum ErrorKind {
27
+ TypeMismatch,
28
+ MissingField,
29
+ Overflow,
30
+ }
31
+
32
+ impl ErrorKind {
33
+ pub fn as_str(self) -> &'static str {
34
+ match self {
35
+ ErrorKind::TypeMismatch => "typeMismatch",
36
+ ErrorKind::MissingField => "missingField",
37
+ ErrorKind::Overflow => "overflow",
38
+ }
39
+ }
40
+ }
41
+
42
+ // ErrorDetail — the structured, recoverable payload a failure carries (scp-error.md "The Error
43
+ // Value"). The LEAF produces it at the wire boundary (it is the only party holding both the declared
44
+ // type and the raw wire datum); the runner transports it verbatim. Concrete fields — no boxed Value,
45
+ // no serialized blob in a string: \`expected_type\` is Portable Type Notation, a rendering of a
46
+ // STATICALLY DECLARED type, so nothing walks a type at runtime.
47
+ #[derive(Debug, Clone, PartialEq, Eq, Default)]
48
+ pub struct ErrorDetail {
49
+ pub kind: Option<ErrorKind>,
50
+ pub model: Option<String>,
51
+ pub field: Option<String>,
52
+ pub expected_type: Option<String>,
53
+ pub actual_wire_type: Option<String>,
54
+ pub raw_value: Option<String>,
55
+ pub context: std::collections::BTreeMap<String, String>,
56
+ }
57
+
58
+ // BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
24
59
  // covered runner returns \`Result<T, BehaviorError>\` over THIS local type instead of a bc-runtime
25
60
  // failure, so the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim
26
- // (byte-equal). The observe companion (test glue, a super:: submodule) bridges this local error to the
27
- // bc-runtime BehaviorError at the observe boundary the covered module itself stays runtime-free.
61
+ // (byte-equal). \`detail\` carries the leaf's structured Error Value across the seam so a caller can
62
+ // log / skip / repair / migrate rather than parse a message. The observe companion (test glue, a
63
+ // super:: submodule) bridges this local error to the bc-runtime failure at the observe boundary —
64
+ // the covered module itself stays runtime-free.
28
65
  #[derive(Debug, Clone)]
29
66
  pub struct BehaviorError {
30
67
  pub code: String,
31
68
  pub message: String,
69
+ pub detail: Option<Box<ErrorDetail>>,
32
70
  }
33
71
 
34
72
  impl BehaviorError {
35
73
  pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
36
- BehaviorError { code: code.into(), message: message.into() }
74
+ BehaviorError { code: code.into(), message: message.into(), detail: None }
75
+ }
76
+ /// The same failure carrying the leaf's structured Error Value.
77
+ pub fn with_detail(code: impl Into<String>, message: impl Into<String>, detail: ErrorDetail) -> Self {
78
+ BehaviorError { code: code.into(), message: message.into(), detail: Some(Box::new(detail)) }
37
79
  }
38
80
  /// The stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type.
39
81
  pub fn code(&self) -> &str {
40
82
  &self.code
41
83
  }
84
+ /// The structured payload, if this failure is about a datum.
85
+ pub fn detail(&self) -> Option<&ErrorDetail> {
86
+ self.detail.as_deref()
87
+ }
42
88
  }
43
89
 
44
90
  impl std::fmt::Display for BehaviorError {
@@ -46,7 +92,75 @@ impl std::fmt::Display for BehaviorError {
46
92
  write!(f, "{}: {}", self.code, self.message)
47
93
  }
48
94
  }
49
- impl std::error::Error for BehaviorError {}`;
95
+ impl std::error::Error for BehaviorError {}
96
+
97
+ // op_failed — wrap a leaf failure as the node's Failure under its Error Policy Kind, carrying the
98
+ // leaf's structured detail through unchanged (the runner transports the Error Value; it does not
99
+ // synthesize or re-interpret it).
100
+ #[allow(dead_code)]
101
+ fn op_failed(node: &str, policy: &str, e: BehaviorError) -> BehaviorError {
102
+ BehaviorError {
103
+ code: "OP_FAILED".to_string(),
104
+ message: format!("operation '{node}' failed under '{policy}' policy: {}", e.message),
105
+ detail: e.detail,
106
+ }
107
+ }
108
+
109
+ // de_type_mismatch / de_missing_field / de_overflow — the de-box mismatch failures the generated decode
110
+ // produces (scp-error.md "The Error Value"). Codes are byte-equal to the runtime outType check
111
+ // (TYPE_MISMATCH / MISSING_PROP) so the covered read stays equivalent to run_behavior; detail.kind
112
+ // distinguishes typeMismatch / missingField / overflow.
113
+ #[allow(dead_code)]
114
+ fn de_type_mismatch(model: &str, field: &str, expected: &str, actual_wire: String, raw: String) -> BehaviorError {
115
+ BehaviorError::with_detail(
116
+ "TYPE_MISMATCH",
117
+ format!("node '{model}': {field}: expected {expected}, got {actual_wire}"),
118
+ ErrorDetail {
119
+ kind: Some(ErrorKind::TypeMismatch),
120
+ model: Some(model.to_string()),
121
+ field: Some(field.to_string()),
122
+ expected_type: Some(expected.to_string()),
123
+ actual_wire_type: Some(actual_wire),
124
+ raw_value: Some(raw),
125
+ context: std::collections::BTreeMap::new(),
126
+ },
127
+ )
128
+ }
129
+
130
+ #[allow(dead_code)]
131
+ fn de_missing_field(model: &str, field: &str, expected: &str) -> BehaviorError {
132
+ BehaviorError::with_detail(
133
+ "MISSING_PROP",
134
+ format!("node '{model}': {field}: required field is absent"),
135
+ ErrorDetail {
136
+ kind: Some(ErrorKind::MissingField),
137
+ model: Some(model.to_string()),
138
+ field: Some(field.to_string()),
139
+ expected_type: Some(expected.to_string()),
140
+ actual_wire_type: None,
141
+ raw_value: None,
142
+ context: std::collections::BTreeMap::new(),
143
+ },
144
+ )
145
+ }
146
+
147
+ #[allow(dead_code)]
148
+ fn de_overflow(model: &str, field: &str, expected: &str, actual_wire: String, raw: String) -> BehaviorError {
149
+ let message = format!("node '{model}': {field}: {raw} is outside the range of {expected}");
150
+ BehaviorError::with_detail(
151
+ "TYPE_MISMATCH",
152
+ message,
153
+ ErrorDetail {
154
+ kind: Some(ErrorKind::Overflow),
155
+ model: Some(model.to_string()),
156
+ field: Some(field.to_string()),
157
+ expected_type: Some(expected.to_string()),
158
+ actual_wire_type: Some(actual_wire),
159
+ raw_value: Some(raw),
160
+ context: std::collections::BTreeMap::new(),
161
+ },
162
+ )
163
+ }`;
50
164
  }
51
165
  function portFieldName(wire) {
52
166
  const safe = wire.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase();
@@ -78,6 +192,431 @@ function nodeMethodName(nodeId) {
78
192
  function inStructName(compName) {
79
193
  return `InNR${pascal(compName)}`;
80
194
  }
195
+ function recordTop(ref) {
196
+ if (ref.kind === "named")
197
+ return { kind: "named", innerName: ref.name };
198
+ if (ref.kind === "arr" && ref.elem.kind === "named")
199
+ return { kind: "arr", innerName: ref.elem.name };
200
+ if (ref.kind === "opt" && ref.inner.kind === "named")
201
+ return { kind: "opt", innerName: ref.inner.name };
202
+ if (ref.kind === "map" && ref.value.kind === "named")
203
+ return { kind: "map", innerName: ref.value.name };
204
+ return null;
205
+ }
206
+ /** deBoxEligible — a componentRef node whose result is a record-containing top (named / arr(named) /
207
+ * opt(named) / map(named)). Its handler returns the wire; the generated decode probes each record. */
208
+ function deBoxEligible(ref) {
209
+ return recordTop(ref) !== null;
210
+ }
211
+ /** rustWireTypeForTop — the Rust handler return type (over the associated Self::Wire) for a record top. */
212
+ function rustWireTypeForTop(top) {
213
+ switch (top.kind) {
214
+ case "named":
215
+ return "Self::Wire";
216
+ case "arr":
217
+ return "Vec<Self::Wire>";
218
+ case "opt":
219
+ return "Option<Self::Wire>";
220
+ case "map":
221
+ return "std::collections::BTreeMap<String, Self::Wire>";
222
+ }
223
+ }
224
+ /** notationOfRef — the Portable Type Notation of a TypeRef (scp-error.md), resolving a named ref to its
225
+ * obj notation via the plan. Baked as the `expectedType` literal (a rendering of the STATICALLY declared
226
+ * type — nothing walks a type at runtime). */
227
+ function notationOfRef(ref, plan) {
228
+ switch (ref.kind) {
229
+ case "scalar":
230
+ return ref.scalar;
231
+ case "opt":
232
+ return `opt(${notationOfRef(ref.inner, plan)})`;
233
+ case "arr":
234
+ return `arr(${notationOfRef(ref.elem, plan)})`;
235
+ case "map":
236
+ return `map(${notationOfRef(ref.value, plan)})`;
237
+ case "named": {
238
+ const decl = findDecl(plan, ref.name);
239
+ return `obj{${decl.fields.map((f) => `${f.name}:${notationOfRef(f.type, plan)}`).join(",")}}`;
240
+ }
241
+ }
242
+ }
243
+ /** emitRustProbeWireTypes — the once-per-module probe enums + WireRow/WireList seam traits. Concrete
244
+ * enums; monomorphized traits (mutual Row/List associated types keep nesting concrete, no dyn/Box). */
245
+ function emitRustProbeWireTypes() {
246
+ return `// Probe<T> / NumProbe — the outcome of classifying one wire attribute against a declared type. Got carries
247
+ // the matched value (a NumProbe's raw numeric text, which the de-box parses + range-checks so overflow is
248
+ // BC's to detect); actual_wire_type is the producer's own wire tag (a free string, e.g. a DynamoDB
249
+ // "S"/"N"/"BOOL"); raw_value is the offending value stringified. Concrete enums — no boxed Value.
250
+ pub enum Probe<T> {
251
+ Got(T),
252
+ Wrong { actual_wire_type: String, raw_value: String },
253
+ Null { actual_wire_type: String, raw_value: String },
254
+ Absent,
255
+ }
256
+
257
+ pub enum NumProbe {
258
+ Got { raw: String, actual_wire_type: String },
259
+ Wrong { actual_wire_type: String, raw_value: String },
260
+ Null { actual_wire_type: String, raw_value: String },
261
+ Absent,
262
+ }
263
+
264
+ // WireRow / WireList — the consumer's wire item / wire array, probed per declared field / element by the
265
+ // generated de-box. The consumer implements them over its wire payload (a DynamoDB AttributeValue map)
266
+ // and classifies each attribute's variant; the de-box owns strictness. Monomorphized (Row/List mutual
267
+ // associated types) — no dyn, no Box, no boxed Value crosses the seam.
268
+ pub trait WireRow: Sized {
269
+ type List: WireList<Row = Self>;
270
+ // the attribute keys present (the entries of a wire map / DynamoDB "M") — iterated to decode a map(V).
271
+ fn keys(&self) -> Vec<String>;
272
+ fn probe_string(&self, field: &str) -> Probe<String>;
273
+ fn probe_number(&self, field: &str) -> NumProbe;
274
+ fn probe_bool(&self, field: &str) -> Probe<bool>;
275
+ fn probe_row(&self, field: &str) -> Probe<Self>;
276
+ fn probe_list(&self, field: &str) -> Probe<Self::List>;
277
+ }
278
+
279
+ pub trait WireList: Sized {
280
+ type Row: WireRow<List = Self>;
281
+ fn len(&self) -> usize;
282
+ fn is_empty(&self) -> bool {
283
+ self.len() == 0
284
+ }
285
+ fn elem_string(&self, i: usize) -> Probe<String>;
286
+ fn elem_number(&self, i: usize) -> NumProbe;
287
+ fn elem_bool(&self, i: usize) -> Probe<bool>;
288
+ fn elem_row(&self, i: usize) -> Probe<Self::Row>;
289
+ fn elem_list(&self, i: usize) -> Probe<Self>;
290
+ }`;
291
+ }
292
+ /** rustDecodeNamedName — the decode function name for a named type (deduped across the module). */
293
+ function rustDecodeNamedName(name) {
294
+ return `decode_wire_${name.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
295
+ }
296
+ /**
297
+ * emitRustDecodeNamed — emit (once, deduped) `decode_wire_<name><W: WireRow>(w: &W) -> Result<Name,
298
+ * BehaviorError>`: probe each declared field from the type SSoT (via the single rustDecodeValue
299
+ * recursion) and build the struct, failing closed with the structured Error Value on a mismatch.
300
+ */
301
+ function emitRustDecodeNamed(name, plan, emitted, out) {
302
+ const fn = rustDecodeNamedName(name);
303
+ if (emitted.has(fn))
304
+ return fn;
305
+ emitted.add(fn);
306
+ const decl = findDecl(plan, name);
307
+ const lines = [];
308
+ lines.push(`// ${fn} — the strict de-box for '${name}': probe each declared field, fail closed on a`);
309
+ lines.push(`// wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
310
+ lines.push(`fn ${fn}<W: WireRow>(w: &W) -> Result<${name}, BehaviorError> {`);
311
+ const ctr = { n: 0 };
312
+ const binds = [];
313
+ for (const f of decl.fields) {
314
+ const rf = rustFieldName(f.name);
315
+ const expr = rustDecodeValue(f.type, "w", rustStrLit(f.name), "row", name, f.name, plan, emitted, out, ctr, 1);
316
+ lines.push(` let ${rf} = ${expr};`);
317
+ binds.push(rf);
318
+ }
319
+ lines.push(` Ok(${name} { ${binds.join(", ")} })`);
320
+ lines.push(`}`);
321
+ out.push(lines.join("\n"));
322
+ return fn;
323
+ }
324
+ /**
325
+ * rustDecodeValue — the SINGLE recursive decode over the full TypeRef grammar (scalar / opt(scalar) /
326
+ * named / arr / map). Returns a Rust expression (a match block) that evaluates to the decoded value of
327
+ * `ref`, obtained by probing `srcExpr` at `keyExpr` (a WireRow field via `probe_*`, srcKind "row"; or a
328
+ * WireList element via `elem_*`, srcKind "list"). Every nested position (arr element, map value, obj
329
+ * field) recurses through THIS function, so arbitrary nesting works by construction. A mismatch does
330
+ * `return Err(<structured error>)`; no panic, no boxed value.
331
+ */
332
+ function rustDecodeValue(ref, srcExpr, keyExpr, srcKind, model, fieldRaw, plan, emitted, out, ctr, indent) {
333
+ const I = " ".repeat(indent);
334
+ const I1 = " ".repeat(indent + 1);
335
+ const ml = rustStrLit(model);
336
+ const fl = rustStrLit(fieldRaw);
337
+ const nl = rustStrLit(notationOfRef(ref, plan));
338
+ const id = ctr.n++;
339
+ const prefix = srcKind === "row" ? "probe_" : "elem_";
340
+ const mismatch = `return Err(de_type_mismatch(${ml}, ${fl}, ${nl}, actual_wire_type, raw_value))`;
341
+ const missing = `return Err(de_missing_field(${ml}, ${fl}, ${nl}))`;
342
+ if (ref.kind === "opt" && ref.inner.kind === "scalar" && ref.inner.scalar !== "null") {
343
+ const inner = ref.inner.scalar;
344
+ if (inner === "int" || inner === "float") {
345
+ const rustT = inner === "int" ? "i64" : "f64";
346
+ return `match ${srcExpr}.${prefix}number(${keyExpr}) {
347
+ ${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
348
+ ${I1} Ok(n) => Some(n),
349
+ ${I1} Err(_) => return Err(de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)),
350
+ ${I1}},
351
+ ${I1}NumProbe::Wrong { actual_wire_type, raw_value } => ${mismatch},
352
+ ${I1}NumProbe::Null { .. } | NumProbe::Absent => None,
353
+ ${I}}`;
354
+ }
355
+ const meth = inner === "string" ? "string" : "bool";
356
+ return `match ${srcExpr}.${prefix}${meth}(${keyExpr}) {
357
+ ${I1}Probe::Got(v) => Some(v),
358
+ ${I1}Probe::Wrong { actual_wire_type, raw_value } => ${mismatch},
359
+ ${I1}Probe::Null { .. } | Probe::Absent => None,
360
+ ${I}}`;
361
+ }
362
+ if (ref.kind === "scalar" && ref.scalar !== "null") {
363
+ if (ref.scalar === "int" || ref.scalar === "float") {
364
+ const rustT = ref.scalar === "int" ? "i64" : "f64";
365
+ return `match ${srcExpr}.${prefix}number(${keyExpr}) {
366
+ ${I1}NumProbe::Got { raw, actual_wire_type } => match raw.parse::<${rustT}>() {
367
+ ${I1} Ok(n) => n,
368
+ ${I1} Err(_) => return Err(de_overflow(${ml}, ${fl}, ${nl}, actual_wire_type, raw)),
369
+ ${I1}},
370
+ ${I1}NumProbe::Wrong { actual_wire_type, raw_value }
371
+ ${I1}| NumProbe::Null { actual_wire_type, raw_value } => ${mismatch},
372
+ ${I1}NumProbe::Absent => ${missing},
373
+ ${I}}`;
374
+ }
375
+ const meth = ref.scalar === "string" ? "string" : "bool";
376
+ return `match ${srcExpr}.${prefix}${meth}(${keyExpr}) {
377
+ ${I1}Probe::Got(v) => v,
378
+ ${I1}Probe::Wrong { actual_wire_type, raw_value }
379
+ ${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
380
+ ${I1}Probe::Absent => ${missing},
381
+ ${I}}`;
382
+ }
383
+ if (ref.kind === "named") {
384
+ const dec = emitRustDecodeNamed(ref.name, plan, emitted, out);
385
+ const rowMeth = srcKind === "row" ? "probe_row" : "elem_row";
386
+ return `match ${srcExpr}.${rowMeth}(${keyExpr}) {
387
+ ${I1}Probe::Got(sub) => ${dec}(&sub)?,
388
+ ${I1}Probe::Wrong { actual_wire_type, raw_value }
389
+ ${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
390
+ ${I1}Probe::Absent => ${missing},
391
+ ${I}}`;
392
+ }
393
+ if (ref.kind === "arr") {
394
+ const listMeth = srcKind === "row" ? "probe_list" : "elem_list";
395
+ const elemExpr = rustDecodeValue(ref.elem, `l${id}`, `i${id}`, "list", model, fieldRaw, plan, emitted, out, ctr, indent + 2);
396
+ return `match ${srcExpr}.${listMeth}(${keyExpr}) {
397
+ ${I1}Probe::Got(l${id}) => {
398
+ ${I1} let mut acc = Vec::with_capacity(l${id}.len());
399
+ ${I1} for i${id} in 0..l${id}.len() {
400
+ ${I1} acc.push(${elemExpr});
401
+ ${I1} }
402
+ ${I1} acc
403
+ ${I1}}
404
+ ${I1}Probe::Wrong { actual_wire_type, raw_value }
405
+ ${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
406
+ ${I1}Probe::Absent => ${missing},
407
+ ${I}}`;
408
+ }
409
+ if (ref.kind === "map") {
410
+ // a declared map(V): the wire value is a producer map (a DynamoDB "M"); enumerate its keys and decode
411
+ // each value as V through THIS same recursion (so map-of-map / map-of-arr nest by construction).
412
+ const rowMeth = srcKind === "row" ? "probe_row" : "elem_row";
413
+ const valExpr = rustDecodeValue(ref.value, `m${id}`, `&k${id}`, "row", model, fieldRaw, plan, emitted, out, ctr, indent + 2);
414
+ return `match ${srcExpr}.${rowMeth}(${keyExpr}) {
415
+ ${I1}Probe::Got(m${id}) => {
416
+ ${I1} let mut acc = std::collections::BTreeMap::new();
417
+ ${I1} for k${id} in m${id}.keys() {
418
+ ${I1} let v${id} = ${valExpr};
419
+ ${I1} acc.insert(k${id}, v${id});
420
+ ${I1} }
421
+ ${I1} acc
422
+ ${I1}}
423
+ ${I1}Probe::Wrong { actual_wire_type, raw_value }
424
+ ${I1}| Probe::Null { actual_wire_type, raw_value } => ${mismatch},
425
+ ${I1}Probe::Absent => ${missing},
426
+ ${I}}`;
427
+ }
428
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: '${fieldRaw}' of '${model}' has type ${JSON.stringify(ref)} which the generated de-box does not decode (supported: scalar / opt(scalar) / named / arr / map).`);
429
+ }
430
+ /** rustDecodeElemName — the element de-box function name for a map/fanout node. */
431
+ function rustDecodeElemName(compName, nodeId) {
432
+ return `decode_wire_elem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
433
+ }
434
+ /** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
435
+ function mapFanoutElemRef(node, typedNodes, plan) {
436
+ const ref = typedNodes.get(node.id);
437
+ if ("fanout" in node)
438
+ return fanoutItemsElemRef(node, ref, plan);
439
+ return mapElemRowRef(node, ref, plan);
440
+ }
441
+ /** mapFanoutElemDeBoxEligible — a map/fanout node whose element ROW is a named record de-boxes each
442
+ * element through the generated decode_wire_elem_* (closing the scan-row silent-default hole). */
443
+ function mapFanoutElemDeBoxEligible(node, typedNodes, plan) {
444
+ return mapFanoutElemRef(node, typedNodes, plan).kind === "named";
445
+ }
446
+ /**
447
+ * emitRustDecodeElemRow — emit (once) `decode_wire_elem_<comp>_<node><W: WireRow>(w: &W) ->
448
+ * Result<RawElemNR<comp><node>, BehaviorError>` for a map/fanout node whose element ROW is a named
449
+ * record: probe each declared field via the SAME rustDecodeValue recursion as record reads and build the
450
+ * concrete RawElemNR struct, failing closed with the structured Error Value on a mismatch.
451
+ */
452
+ function emitRustDecodeElemRow(comp, node, elemRowRef, plan, emitted, out) {
453
+ const nodeId = node.id;
454
+ const fn = rustDecodeElemName(comp.name, nodeId);
455
+ if (emitted.has(fn))
456
+ return fn;
457
+ emitted.add(fn);
458
+ const retType = rawElemStructName(comp.name, nodeId);
459
+ const decl = findDecl(plan, elemRowRef.name);
460
+ const lines = [];
461
+ lines.push(`// ${fn} — the strict de-box for a '${nodeId}' element row: probe each declared field, fail`);
462
+ lines.push(`// closed on a wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
463
+ lines.push(`fn ${fn}<W: WireRow>(w: &W) -> Result<${retType}, BehaviorError> {`);
464
+ const ctr = { n: 0 };
465
+ const binds = [];
466
+ for (const f of decl.fields) {
467
+ const rf = rustFieldName(f.name);
468
+ const expr = rustDecodeValue(f.type, "w", rustStrLit(f.name), "row", elemRowRef.name, f.name, plan, emitted, out, ctr, 1);
469
+ lines.push(` let ${rf} = ${expr};`);
470
+ binds.push(rf);
471
+ }
472
+ lines.push(` Ok(${retType} { ${binds.join(", ")} })`);
473
+ lines.push(`}`);
474
+ out.push(lines.join("\n"));
475
+ return fn;
476
+ }
477
+ /** emitRustNodeDecodeFns — emit the decode functions for every de-box-eligible node: a componentRef
478
+ * record read (decode_wire_<name>) OR a map/fanout element row (decode_wire_elem_<comp>_<node>). */
479
+ /** rustDecodeResultName — the top-result decode function name for a de-box componentRef node. */
480
+ function rustDecodeResultName(compName, nodeId) {
481
+ return `decode_wire_result_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase()}`;
482
+ }
483
+ /**
484
+ * emitRustDecodeWireResult — emit (once) `decode_wire_result_<comp>_<node><W: WireRow>(wire: <WireArg>)
485
+ * -> Result<<ResultType>, BehaviorError>` for a componentRef node whose result is a record-containing
486
+ * top: it decodes each record via decode_wire_<name> and assembles the top (record / Vec / Option /
487
+ * BTreeMap). One path covers named / arr(named) / opt(named) / map(named).
488
+ */
489
+ function emitRustDecodeWireResult(comp, node, ref, plan, emitted, out) {
490
+ const top = recordTop(ref);
491
+ const fn = rustDecodeResultName(comp.name, node.id);
492
+ if (emitted.has(fn))
493
+ return fn;
494
+ emitted.add(fn);
495
+ const innerDecode = emitRustDecodeNamed(top.innerName, plan, emitted, out);
496
+ const resultType = renderTypeRef(ref);
497
+ const lines = [];
498
+ lines.push(`// ${fn} — decode the node's record-containing '${top.kind}' result from the wire (each record`);
499
+ lines.push(`// via ${innerDecode}); a de-box mismatch fails closed with the structured Error Value.`);
500
+ switch (top.kind) {
501
+ case "named":
502
+ lines.push(`fn ${fn}<W: WireRow>(wire: W) -> Result<${resultType}, BehaviorError> {`);
503
+ lines.push(` ${innerDecode}(&wire)`);
504
+ break;
505
+ case "arr":
506
+ lines.push(`fn ${fn}<W: WireRow>(wire: Vec<W>) -> Result<${resultType}, BehaviorError> {`);
507
+ lines.push(` let mut out = Vec::with_capacity(wire.len());`);
508
+ lines.push(` for w in wire {`);
509
+ lines.push(` out.push(${innerDecode}(&w)?);`);
510
+ lines.push(` }`);
511
+ lines.push(` Ok(out)`);
512
+ break;
513
+ case "opt":
514
+ lines.push(`fn ${fn}<W: WireRow>(wire: Option<W>) -> Result<${resultType}, BehaviorError> {`);
515
+ lines.push(` match wire {`);
516
+ lines.push(` Some(w) => Ok(Some(${innerDecode}(&w)?)),`);
517
+ lines.push(` None => Ok(None),`);
518
+ lines.push(` }`);
519
+ break;
520
+ case "map":
521
+ lines.push(`fn ${fn}<W: WireRow>(wire: std::collections::BTreeMap<String, W>) -> Result<${resultType}, BehaviorError> {`);
522
+ lines.push(` let mut out = std::collections::BTreeMap::new();`);
523
+ lines.push(` for (k, w) in wire {`);
524
+ lines.push(` out.insert(k, ${innerDecode}(&w)?);`);
525
+ lines.push(` }`);
526
+ lines.push(` Ok(out)`);
527
+ break;
528
+ }
529
+ lines.push(`}`);
530
+ out.push(lines.join("\n"));
531
+ return fn;
532
+ }
533
+ function emitRustNodeDecodeFns(comp, typedNodes, plan, emitted, out) {
534
+ for (const n of comp.body) {
535
+ if ("cond" in n)
536
+ continue;
537
+ if ("map" in n || "fanout" in n) {
538
+ const node = n;
539
+ if (!mapFanoutElemDeBoxEligible(node, typedNodes, plan))
540
+ continue;
541
+ emitRustDecodeElemRow(comp, node, mapFanoutElemRef(node, typedNodes, plan), plan, emitted, out);
542
+ continue;
543
+ }
544
+ const ref = typedNodes.get(n.id);
545
+ if (ref === undefined)
546
+ continue;
547
+ const top = recordTop(ref);
548
+ if (top === null)
549
+ continue;
550
+ // a NAMED top decodes in place via decode_wire_<name>; an arr/opt/map top assembles via
551
+ // decode_wire_result_* (which recurses into decode_wire_<name> per record).
552
+ if (top.kind === "named")
553
+ emitRustDecodeNamed(top.innerName, plan, emitted, out);
554
+ else
555
+ emitRustDecodeWireResult(comp, n, ref, plan, emitted, out);
556
+ }
557
+ }
558
+ /** containsNamed — whether a TypeRef transitively contains a named record (through arr / opt / map). */
559
+ function containsNamed(ref) {
560
+ switch (ref.kind) {
561
+ case "scalar":
562
+ return false;
563
+ case "named":
564
+ return true;
565
+ case "opt":
566
+ return containsNamed(ref.inner);
567
+ case "arr":
568
+ return containsNamed(ref.elem);
569
+ case "map":
570
+ return containsNamed(ref.value);
571
+ }
572
+ }
573
+ /**
574
+ * assertDeBoxableResults — fail closed (never a silent trust) for a covered node whose de-boxable result
575
+ * is a NON-named top (arr / opt / map) that transitively contains a record: the generated de-box decodes
576
+ * records only under a NAMED top (decode_wire_* / decode_wire_elem_*), so a record inside a non-named top
577
+ * would be materialized on the trusting `.val` path and silently default its wire fields. That is the
578
+ * silent-default red line forbids, so it is a loud generation-time error (native-codegen-standard
579
+ * §2). A NAMED top (de-boxed) or a pure-scalar non-named top (no records to de-box) is fine.
580
+ */
581
+ /** readsAPriorNode — a componentRef node is a DATAFLOW consumer iff at least one input port is a ref
582
+ * headed by a PRIOR BODY NODE (it reads a prior node's already-native result, #129), rather than a fresh
583
+ * external wire read whose ports come from input params / static / an element ($as) binding. Same
584
+ * head-is-a-body-node signal priorNodeArrElemInfo (#129) uses to lower a prior-node array feed. */
585
+ function readsAPriorNode(node, typedNodes) {
586
+ for (const expr of Object.values(node.ports ?? {})) {
587
+ const e = classifyExpr(expr);
588
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0]))
589
+ return true;
590
+ }
591
+ return false;
592
+ }
593
+ function assertDeBoxableResults(comp, typedNodes, plan) {
594
+ for (const n of comp.body) {
595
+ if ("cond" in n)
596
+ continue;
597
+ const isMapFanout = "map" in n || "fanout" in n;
598
+ const ref = isMapFanout ? mapFanoutElemRef(n, typedNodes, plan) : typedNodes.get(n.id);
599
+ const deBoxed = isMapFanout ? ref.kind === "named" : recordTop(ref) !== null;
600
+ if (deBoxed)
601
+ continue;
602
+ const nodeId = n.id;
603
+ if (containsNamed(ref)) {
604
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: node '${nodeId}' shape ${JSON.stringify(ref)} contains a record the de-box does not decode (a deeper nesting than a direct named / arr / opt / map of a named record). It would trust the record's wire fields (silent default) — the silent-default red line forbids. Fail closed: reshape so the record is a direct de-box top, or extend the recursion for this nesting.`);
605
+ }
606
+ // a map/fanout element is ALWAYS a per-element wire read; only a named record is de-boxed
607
+ // (decode_wire_elem_*). A scalar / scalar-collection element materializes from the trusting per-element
608
+ // path — a wrong wire value silent-defaults. Fail closed.
609
+ if (isMapFanout) {
610
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: map/fanout node '${nodeId}' element ${JSON.stringify(ref)} is a scalar / scalar-collection per-element wire read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the element as a record (de-boxed).`);
611
+ }
612
+ // a pure-scalar / scalar-collection top on a HANDLER-BACKED componentRef wire read trusts the typed
613
+ // materialization (a wrong wire value silent-defaults). A DATAFLOW consumer (a port fed by a prior
614
+ // node) computes from already-native data and generates.
615
+ if (!readsAPriorNode(n, typedNodes)) {
616
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust strict de-box: node '${nodeId}' result top ${JSON.stringify(ref)} is a handler-backed scalar / scalar-collection read that materializes on the trusting typed path — a wrong wire value would silent-default (diverging from run_behavior's outType check). Fail closed: declare the result as a record (de-boxed) or feed it as dataflow from a prior node.`);
617
+ }
618
+ }
619
+ }
81
620
  function staticArrElems(node) {
82
621
  if (node === null || typeof node !== "object" || Array.isArray(node))
83
622
  return null;
@@ -88,11 +627,60 @@ function staticArrElems(node) {
88
627
  return Array.isArray(arg) ? arg : null;
89
628
  }
90
629
  /**
91
- * portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free), read off the
92
- * ONE port lowering (compilePortNode / native-expr). The covered ports struct carries NO `Value` field:
93
- * every port lowers to a concrete Rust type, and a port that cannot lower FAILS CLOSED. The field type is
94
- * the compiled expression's TypeRef, so it can never disagree with the initializer (emitPortInit), which
95
- * is the SAME lowering read for its value. `where` names the emission site for the error.
630
+ * portFieldRustType — the NATIVE Rust type for a covered port field (bc#90 / runtime-free). The covered
631
+ * ports struct carries NO `Value` field: every port lowers to a concrete Rust type.
632
+ /** rustOwn the ownership renderer native-expr applies at a resolved ref LEAF: an array field is cloned
633
+ * (owned) when `ownArrays` (a port) or borrowed (a cond `len`); a Copy scalar behind a guard is deref'd
634
+ * (unless a field was already accessed by value); anything else clones. */
635
+ function rustOwn(guard, ownArrays) {
636
+ return (expr, ref, fieldAccessed) => {
637
+ if (ref.kind === "arr")
638
+ return ownArrays ? `${expr}.clone()` : expr;
639
+ if (typeRefIsCopy(ref))
640
+ return guard && !fieldAccessed ? `*${expr}` : expr;
641
+ return `${expr}.clone()`;
642
+ };
643
+ }
644
+ /** nativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads (cond `if`/
645
+ * branches, a map/fanout `when` guard, AND a port). A head is a `$as` element binding, a prior body node's
646
+ * typed cell (behind a RefCell borrow guard), or an input port (a by-value struct field). #139: the input
647
+ * ref comes from the inputPortTypeRef SSoT so an OPTIONAL port resolves as `{opt:…}` (native Option<T>). */
648
+ function nativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
649
+ const ownArrays = opts?.ownArrays === true;
650
+ return (head) => {
651
+ if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
652
+ return { expr: opts.asBase, ref: opts.asBinding.ref, own: rustOwn(true, ownArrays) };
653
+ }
654
+ if (isPrior(head)) {
655
+ const ref = typedNodes.get(head);
656
+ return { expr: `${typedCell(head)}.borrow()`, ref, own: rustOwn(true, ownArrays) };
657
+ }
658
+ const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
659
+ if (ref === undefined)
660
+ return null;
661
+ return { expr: `in_.${rustFieldName(head)}`, ref, own: rustOwn(false, ownArrays) };
662
+ };
663
+ }
664
+ /** compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
665
+ * native-expr compiler: the ONE port lowering. The port's field TYPE is the compiled `ref`, its
666
+ * initializer the compiled `expr` — derived from a SINGLE lowering, so they cannot disagree. A port that
667
+ * cannot lower FAILS CLOSED; a FALLIBLE lowering (hoists statements) is returned as-is (only emitPortInit
668
+ * rejects it). Reuses the same nativeResolveHead cond/guard/port share, with `ownArrays` (a port owns). */
669
+ function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
670
+ const resolveHead = nativeResolveHead(comp, plan, typedNodes, isPrior, { ...opts, ownArrays: true });
671
+ try {
672
+ return new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
673
+ }
674
+ catch (e) {
675
+ if (!(e instanceof GeneratorFailure))
676
+ throw e;
677
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Rust value: ${e.message}`);
678
+ }
679
+ }
680
+ /**
681
+ * portFieldRustType — the NATIVE Rust type for a covered port field, read off the ONE port lowering
682
+ * (compilePortNode / native-expr). Field type = the compiled TypeRef, so it can never disagree with the
683
+ * initializer (emitPortInit), which reads the SAME lowering for its value. `where` names the site.
96
684
  */
97
685
  function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
98
686
  const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
@@ -105,12 +693,17 @@ function portFieldRustType(node, comp, plan, typedNodes, where = "port", opts) {
105
693
  * reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
106
694
  * per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
107
695
  */
108
- function emitPortsStruct(comp, node, plan, typedNodes, asBinding) {
696
+ function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
109
697
  const name = portsStructName(comp.name, node.id);
110
698
  const portNames = Object.keys(node.ports);
111
699
  if (portNames.length === 0) {
112
700
  return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
113
701
  }
702
+ // a node WITH ports always resolves through the one port lowering, which needs the type plan + the
703
+ // prior-node map (both are supplied by every caller — narrow the optional params for the fields below).
704
+ if (plan === undefined || typedNodes === undefined) {
705
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native: ports struct for node '${node.id}' needs a type plan + prior-node map`);
706
+ }
114
707
  const types = portNames.map((p) => portFieldRustType(node.ports[p], comp, plan, typedNodes, `port '${p}' of node '${node.id}'`, asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined));
115
708
  const fields = portNames
116
709
  .map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
@@ -135,7 +728,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
135
728
  // #108: element ports may read the `$as` binding — resolve the over element type for native typing.
136
729
  const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
137
730
  const asBinding = { name: m.as, ref: elemRef, plan };
138
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes, asBinding);
731
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
139
732
  return `${elemStruct}
140
733
 
141
734
  // ${batch} — CONCRETE batched ports for map '${node.id}': the Vec of per-element CONCRETE ports structs
@@ -153,7 +746,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
153
746
  const batch = `${name}Batch`;
154
747
  const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
155
748
  const asBinding = { name: f.as, ref: elemRef, plan };
156
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes, asBinding);
749
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
157
750
  return `${elemStruct}
158
751
 
159
752
  // ${batch} — CONCRETE batched ports for fanout '${node.id}': the Vec of per-id CONCRETE ports structs.
@@ -173,6 +766,20 @@ let rustExprTempSeq = 0;
173
766
  /** module-level accumulator: set when the native-expr compiler emits a fallible helper call (so emit()
174
767
  * bakes the helper library). Reset at the start of each emit(). */
175
768
  const rustExprUsed = { helpers: false };
769
+ /** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
770
+ * `let t = E; t` collapses to `E` (clippy `let_and_return`). */
771
+ function rustBlockBody(stmts, expr) {
772
+ const last = stmts[stmts.length - 1];
773
+ const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
774
+ if (m !== null && m[1] === expr)
775
+ return [...stmts.slice(0, -1), m[2]].join(" ");
776
+ return [...stmts, rustStripOuterParens(expr)].join(" ");
777
+ }
778
+ /** Drop a TRAILING `.clone()` — for a position that only reads the value (a presence test), so an owning
779
+ * clone added by a head resolver is pure waste. Only a clone at the very END is dropped. */
780
+ function rustStripTrailingClone(expr) {
781
+ return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
782
+ }
176
783
  /** makeRustExprBackend — renders native-expr for Rust. `resolveHead` maps a ref head to an OWNED typed
177
784
  * base expr (a prior-node cell clone / an input field / a $as element). Sets rustExprUsed.helpers when
178
785
  * a fallible helper is emitted. The runner is Result<_, BehaviorError>, so hoistFallible uses `?`. */
@@ -216,47 +823,39 @@ function makeRustExprBackend(resolveHead, plan) {
216
823
  notOp: (a) => `(!${a})`,
217
824
  structLit: (name, inits) => `${name} { ${inits.map((i) => `${rustFieldName(i.field)}: ${i.expr}`).join(", ")} }`,
218
825
  arrLit: (elemTy, items) => `{ let __v: Vec<${elemTy}> = vec![${items.join(", ")}]; __v }`,
219
- // a projection port of static string literals — bare `&'static str` literals, ZERO heap allocation
220
- // (the field is `Vec<&'static str>`, so `vec!["a", "b"]` infers `&'static str` elements).
826
+ // a projection port of static string literals — `vec!["a", "b"]` infers `&'static str` (the field is
827
+ // Vec<&'static str>), ZERO heap allocation.
221
828
  staticStrArr: (literals) => ({ expr: `vec![${literals.map((s) => rustStrLit(s)).join(", ")}]`, type: "Vec<&'static str>" }),
222
- // an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
223
- // position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
224
- optNone: (innerTy) => `Option::<${innerTy}>::None`,
225
- // opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
226
- // A presence test only READS the discriminant, so an owning clone of the operand (which a head
227
- // resolver adds so the value can be moved into a port) is dropped here — testing `Option<String>`
228
- // must not heap-allocate. Mirrors the go twin's zero-cost `!= nil`.
229
- optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
230
- optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
231
- // opt (`Option<T>`) defaulted to a PURE default — it cannot fail and has no effect, so its
232
- // evaluation order is UNOBSERVABLE and both forms below are correct (a FALLIBLE default, where
233
- // laziness IS observable, goes to optCoalesceGuard instead). `unwrap_or` is EAGER — fine for a plain
234
- // literal/copy. A default that allocates (an owned `String`, a struct/vec literal) uses
235
- // `unwrap_or_else`, whose closure defers the allocation to the absent branch: not a semantic
236
- // requirement here, but pointless work otherwise — and what clippy `or_fun_call` asks for.
829
+ // opt (Option<T>) defaulted to a PURE default eager unwrap_or for a plain copy/path, else the lazy
830
+ // unwrap_or_else closure (defers an allocating default to the absent branch; clippy or_fun_call).
237
831
  optUnwrapOr: (optExpr, defaultExpr, _innerTy) => /^[A-Za-z0-9_.:]+$/.test(defaultExpr) ? `${optExpr}.unwrap_or(${defaultExpr})` : `${optExpr}.unwrap_or_else(|| ${defaultExpr})`,
238
832
  // opt defaulted to a FALLIBLE default — a `match`, NOT a closure: the default's hoisted `?` early
239
- // returns must propagate out of the ENCLOSING fn, which `unwrap_or_else`'s closure would swallow
240
- // (it would try to return from the closure). Only the None arm evaluates the default.
833
+ // returns must propagate out of the ENCLOSING fn. Only the None arm evaluates the default.
241
834
  optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
242
835
  `let ${temp}: ${ty} = match ${optExpr} {`,
243
836
  ` Some(${bindTemp}) => ${bindTemp},`,
244
837
  ` None => { ${rustBlockBody(dStmts, dExpr)} }`,
245
838
  `};`,
246
839
  ],
840
+ // an opt (`Option<T>`) NONE literal. Annotate as `Option::<T>::None` so it type-infers in ANY
841
+ // position (a bare `None` can be ambiguous; the explicit turbofish keeps it a fully-typed value).
842
+ optNone: (innerTy) => `Option::<${innerTy}>::None`,
843
+ // opt (`Option<T>`) null-presence test: present = `expr.is_some()`, absent = `expr.is_none()`.
844
+ optIsSome: (expr) => `${rustStripTrailingClone(expr)}.is_some()`,
845
+ optIsNone: (expr) => `${rustStripTrailingClone(expr)}.is_none()`,
247
846
  ternary: (cond, t, e, _ty) => `(if ${rustStripOuterParens(cond)} { ${rustStripOuterParens(t)} } else { ${rustStripOuterParens(e)} })`,
248
847
  shortCircuitBool: (op, temp, left, rStmts, rExpr) => {
249
848
  // and: let temp = if left { <rStmts> rExpr } else { false }; or: if left { true } else { <rStmts> rExpr }.
250
849
  // build a block that runs rStmts then yields rExpr (parens stripped — block-tail is not a sub-expr).
251
- const rblock = `{ ${rustBlockBody(rStmts, rExpr)} }`;
850
+ const rblock = `{ ${rStmts.join(" ")} ${rustStripOuterParens(rExpr)} }`;
252
851
  if (op === "and") {
253
852
  return [`let ${temp}: bool = if ${rustStripOuterParens(left)} ${rblock} else { false };`];
254
853
  }
255
854
  return [`let ${temp}: bool = if ${rustStripOuterParens(left)} { true } else ${rblock};`];
256
855
  },
257
856
  condGuard: (temp, ty, cond, tStmts, tExpr, eStmts, eExpr) => {
258
- const tblock = `{ ${rustBlockBody(tStmts, tExpr)} }`;
259
- const eblock = `{ ${rustBlockBody(eStmts, eExpr)} }`;
857
+ const tblock = `{ ${tStmts.join(" ")} ${rustStripOuterParens(tExpr)} }`;
858
+ const eblock = `{ ${eStmts.join(" ")} ${rustStripOuterParens(eExpr)} }`;
260
859
  return [`let ${temp}: ${ty} = if ${rustStripOuterParens(cond)} ${tblock} else ${eblock};`];
261
860
  },
262
861
  };
@@ -265,17 +864,6 @@ function makeRustExprBackend(resolveHead, plan) {
265
864
  function rustSnake(name) {
266
865
  return name.replace(/([a-z])([A-Z0-9])/g, "$1_$2").replace(/([0-9])([a-z])/g, "$1_$2").toLowerCase();
267
866
  }
268
- /** rustBlockBody — the body of a block-expression that runs `stmts` then yields `expr`. A trailing
269
- * `let t = E; t` is collapsed to `E`: binding a temp only to return it on the next line is clippy's
270
- * `let_and_return` (an error under `-D warnings`), and the hoisted-fallible shape produces exactly that
271
- * whenever the block's tail IS the hoisted temp. */
272
- function rustBlockBody(stmts, expr) {
273
- const last = stmts[stmts.length - 1];
274
- const m = last !== undefined ? /^let\s+([A-Za-z0-9_]+)(?::\s*[^=]+)?\s*=\s*(.+);$/.exec(last) : null;
275
- if (m !== null && m[1] === expr)
276
- return [...stmts.slice(0, -1), m[2]].join(" ");
277
- return [...stmts, rustStripOuterParens(expr)].join(" ");
278
- }
279
867
  /** strip ONE redundant outer paren pair from an expr (clippy `if (cond)` unused_parens). Only when the
280
868
  * whole string is a single balanced `( … )` — a `(a) && (b)` stays untouched. */
281
869
  function rustStripOuterParens(expr) {
@@ -293,12 +881,6 @@ function rustStripOuterParens(expr) {
293
881
  }
294
882
  return expr.slice(1, -1);
295
883
  }
296
- /** Drop a TRAILING `.clone()` from an expression — for a position that only reads the value (no move),
297
- * so an owning clone added by a head resolver is pure waste. Only a clone at the very END is dropped: a
298
- * `.clone()` mid-path (`t_x.borrow().clone().field`) still owns the field access that follows it. */
299
- function rustStripTrailingClone(expr) {
300
- return expr.endsWith(".clone()") ? expr.slice(0, -".clone()".length) : expr;
301
- }
302
884
  /** Rust float literal (finite; integral gets `.0`). */
303
885
  function rustFloatLitExpr(n) {
304
886
  if (Number.isInteger(n))
@@ -392,13 +974,14 @@ fn bc_expr_str_ge(a: &str, b: &str) -> bool { bc_expr_cmp_cp(a, b) != std::cmp::
392
974
  // ── CONCRETE per-node row structs + row→outType copiers (mirror the Go concrete contract) ──────────
393
975
  //
394
976
  // Each covered node has a CONCRETE row struct `RawRowNR<Comp><Node>` whose fields are the node's projected
395
- // outType fields (typed, flattened) plus a per-node error signal (is_error/err). A named-struct outType
977
+ // outType fields (typed, flattened). A row models a SUCCESS: failure travels the Result's Err arm as a
978
+ // concrete BehaviorError carrying the leaf's structured Error Value. A named-struct outType
396
979
  // flattens to one Rust field per outType field; a scalar/arr/opt node carries a single `val: <typed>`
397
980
  // payload. The per-component `HandlerNR<Comp>` trait has one method per node returning that concrete
398
981
  // struct; the runner reads `row.<field>` DIRECTLY (no `RawValue`, no `match`, no `Value::`) and copies
399
982
  // each field into the node's outType struct local.
400
983
  /** emitRawRowStructs — emit the concrete `RawRowNR<Comp><Node>` struct per covered node (+ per-element
401
- * `RawElemNR<Comp><Node>` for a covered map). Every row carries the per-node error signal (is_error/err). */
984
+ * `RawElemNR<Comp><Node>` for a covered map). A row models a SUCCESS; failure is the Result's Err arm. */
402
985
  function emitRawRowStructs(comp, typedNodes, plan) {
403
986
  const parts = [];
404
987
  for (const n of comp.body) {
@@ -409,7 +992,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
409
992
  const elemRowRef = fanoutItemsElemRef(n, ref, plan);
410
993
  parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
411
994
  parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
412
- `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
995
+ `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
413
996
  continue;
414
997
  }
415
998
  if ("map" in n) {
@@ -417,22 +1000,20 @@ function emitRawRowStructs(comp, typedNodes, plan) {
417
1000
  const elemRowRef = mapElemRowRef(n, arrRef, plan);
418
1001
  parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
419
1002
  parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
420
- `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub is_error: bool,\n pub err: String,\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
1003
+ `#[derive(Default)]\npub struct ${rawRowStructName(comp.name, n.id)} {\n pub rows: Vec<${rawElemStructName(comp.name, n.id)}>,\n}`);
421
1004
  continue;
422
1005
  }
423
1006
  parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
424
1007
  }
425
1008
  return parts.join("\n\n");
426
1009
  }
427
- /** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload;
428
- * always carry is_error/err. Derives Default so a handler can build an error row cheaply (`..Default`). */
1010
+ /** Emit one concrete row struct: flatten a named outType to typed fields, else a single `val` payload.
1011
+ * A row models a SUCCESS failure is the Result's Err arm, so no in-band error signal. */
429
1012
  function emitOneRowStruct(name, ref, plan) {
430
1013
  const lines = [];
431
- lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
1014
+ lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType).`);
432
1015
  lines.push(`#[derive(Default)]`);
433
1016
  lines.push(`pub struct ${name} {`);
434
- lines.push(` pub is_error: bool,`);
435
- lines.push(` pub err: String,`);
436
1017
  if (ref.kind === "named") {
437
1018
  const decl = findDecl(plan, ref.name);
438
1019
  for (const f of decl.fields) {
@@ -507,6 +1088,19 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
507
1088
  lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
508
1089
  lines.push(`// node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
509
1090
  lines.push(`pub trait ${handlerTraitName(comp.name)} {`);
1091
+ // a record-read node returns the consumer's WIRE (an associated `Wire: WireRow`); the generated
1092
+ // decode turns it into the concrete row. Declared once when the component has a de-box-eligible node.
1093
+ // The `+ Send` bound lets the wire cross the parallel-stage runner's scoped fan-out threads (the old
1094
+ // concrete row was Send; the associated type needs the bound stated explicitly).
1095
+ const hasDeBox = comp.body.some((n) => {
1096
+ if ("cond" in n)
1097
+ return false;
1098
+ if ("map" in n || "fanout" in n)
1099
+ return mapFanoutElemDeBoxEligible(n, typedNodes, plan);
1100
+ return deBoxEligible(typedNodes.get(n.id));
1101
+ });
1102
+ if (hasDeBox)
1103
+ lines.push(` type Wire: WireRow + Send;`);
510
1104
  for (const n of comp.body) {
511
1105
  if ("cond" in n)
512
1106
  continue; // #108: a cond node has no handler method (pure Expression).
@@ -514,21 +1108,29 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
514
1108
  const fnKw = fnKwFor(n.id);
515
1109
  const bt = boundRustType(n, typedNodes, plan);
516
1110
  if ("fanout" in n) {
517
- // fanout (v3): one batched dispatch of the deduped id list batch ports IN, aligned batch row OUT.
1111
+ // fanout (v3): one batched dispatch of the deduped id list. A record element returns the aligned
1112
+ // element WIRES the runner de-boxes (decode_wire_elem_*) then dedupe/drops. Each aligned position is
1113
+ // Option<Self::Wire> — None = a DANGLING id (the runner maps it to the zero elem row).
518
1114
  const portsT = `${portsStructName(comp.name, n.id)}Batch`;
519
- const retT = rawRowStructName(comp.name, n.id);
520
- lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
1115
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
1116
+ const retT = elemDeBox ? "Vec<Option<Self::Wire>>" : rawRowStructName(comp.name, n.id);
1117
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
521
1118
  continue;
522
1119
  }
523
1120
  if ("map" in n) {
524
1121
  const m = n.map;
525
1122
  const batched = m.batched === true;
526
1123
  const portsT = batched ? `${portsStructName(comp.name, n.id)}Batch` : portsStructName(comp.name, n.id);
527
- const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
528
- lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Option<${retT}>;`);
1124
+ // a record-element map returns the element WIRE(s): batched → Vec<Self::Wire>, nonbatched → Self::Wire.
1125
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
1126
+ const retT = elemDeBox ? (batched ? "Vec<Self::Wire>" : "Self::Wire") : batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
1127
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsT}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
529
1128
  continue;
530
1129
  }
531
- lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Option<${rawRowStructName(comp.name, n.id)}>;`);
1130
+ const ref = typedNodes.get(n.id);
1131
+ const top = ref !== undefined ? recordTop(ref) : null;
1132
+ const retT = top !== null ? rustWireTypeForTop(top) : rawRowStructName(comp.name, n.id);
1133
+ lines.push(` ${fnKw} ${method}(&self, ports: &${portsStructName(comp.name, n.id)}, bound: ${bt}) -> Result<${retT}, BehaviorError>;`);
532
1134
  }
533
1135
  lines.push(`}`);
534
1136
  return lines.join("\n");
@@ -541,18 +1143,16 @@ function emitHandlerTrait(comp, typedNodes, plan, ap) {
541
1143
  function mapElemMaterializerName(compName, nodeId) {
542
1144
  return `materialize_map_elem_${sanitize(compName)}_${typedCell(nodeId)}`;
543
1145
  }
544
- /** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`, normalizing the two IR
545
- * conventions to the one array shape every downstream site assumes (`typedNodes.get(mapId)` is an arr):
546
- * - the RECORDER SoT (authoring.ts recordMap): `node.outType` is the ELEMENT (pre-array-wrap) wrap
547
- * it to `{arr:element}` here (the map produces `[]element`), matching type-gate's outputType wrap
548
- * (`"map" in n ? {arr:ot} : ot`). This is the declarative-authoring form (graphddb batchWrite/forEach).
549
- * - legacy hand-built IR: `node.outType` is already the `{arr:element}` produced form take it as-is.
550
- * A map ELEMENT is never itself an `{arr:…}` in the covered corpus, so the `arr`-keyed discriminator is
551
- * unambiguous. Both converge to the SAME produced-array ref, so existing goldens stay byte-identical. */
1146
+ /** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`.
1147
+ *
1148
+ * A map node's `outType` is its ELEMENT type (scp-error.md "Map element type"), so the produced array
1149
+ * is `{arr: element}`. This is the same reading the compiler applies when it resolves what a node
1150
+ * result reference has (`type-gate`: `"map" in n ? {arr:ot} : ot`) the emitter follows that SSoT
1151
+ * rather than inspecting the annotation's shape, so an element that is ITSELF an `arr` stays
1152
+ * expressible (element `arr(T)` produces `arr(arr(T))`). */
552
1153
  function mapNodeArrRef(node, plan) {
553
1154
  const outT = node.outType;
554
- const isArr = outT !== null && typeof outT === "object" && "arr" in outT;
555
- return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
1155
+ return deriveTypeRef({ arr: outT }, plan);
556
1156
  }
557
1157
  /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
558
1158
  * type (into) or the element outType directly (no-into). This is the type of RawElemNR<Comp><Node>. */
@@ -664,26 +1264,42 @@ function reconstructR(e) {
664
1264
  return e.node;
665
1265
  }
666
1266
  }
667
- /** Does a port lower to a native value? The ELIGIBILITY gate ASKS the one port lowering (compilePortNode
668
- * / native-expr) whether it can lower this port, rather than re-deriving the shape rules so the gate
669
- * and the emitter can never disagree. A port that does not lower (or lowers fallibly) fails closed. */
670
- function portIsStatic(node, comp, plan, typedNodes, asBinding) {
671
- try {
672
- compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
673
- return true;
674
- }
675
- catch {
676
- return false; // an eligibility probe: any lowering failure not native-covered here.
677
- }
678
- }
679
- /** the prior-node typed-cell TypeRef map for a component (control-gate nodes are typeless; a map node
680
- * carries its produced-array ref). Shared by coverage, the struct surface, and the runner. */
1267
+ /**
1268
+ * numLiteralRustExpr a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
1269
+ * `classifyExpr` leaves a bare number `dynamic` (its checked int/float range rules are the evaluate
1270
+ * SSoT), so it never reaches the static str/bool/null branch — but a number literal IS statically
1271
+ * resolvable to a native `Value`: the SAME range check the runtime applies (integral literal in the
1272
+ * safe 2^53-1 window Value::Int; a fractional/exponent literal the checked Value::Float) is
1273
+ * performed HERE at generation time. Returns the Rust expression for the literal's Value, or null
1274
+ * when the node is not a bare number OR an integral literal is out of the safe window (keeping the
1275
+ * runtime INVALID_LITERAL semantics — the port is not covered natively, the component falls through).
1276
+ * Mirrors behavior_contracts eval of a bare number literal exactly.
1277
+ */
1278
+ const NUM_SAFE_INT_R = 9007199254740991; // 2^53 - 1 (expr safeInt)
1279
+ function numLiteralRustExpr(node) {
1280
+ if (typeof node !== "number" || !Number.isFinite(node))
1281
+ return null;
1282
+ if (Number.isInteger(node)) {
1283
+ if (node < -NUM_SAFE_INT_R || node > NUM_SAFE_INT_R)
1284
+ return null; // out of safe window → not covered
1285
+ return `Value::Int(${node}i64)`;
1286
+ }
1287
+ // fractional / exponent literal → checked f64 (finite, guaranteed above).
1288
+ return `Value::Float(${node}f64)`;
1289
+ }
1290
+ /** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
1291
+ * resolvable to a native Value with no Obj / no evaluate interpreter. */
1292
+ /** planForComp — a TypePlan for a single component (its named types are component-scoped). */
1293
+ function planForComp(comp) {
1294
+ return buildTypePlan({ irVersion: 1, exprVersion: 2, components: [comp] });
1295
+ }
1296
+ /** buildTypedNodes — the prior-node typed-cell TypeRef map (control-gate nodes typeless; a map node
1297
+ * carries its produced-array ref). An untyped node is omitted. */
681
1298
  function buildTypedNodes(comp, plan) {
682
1299
  const typedNodes = new Map();
683
1300
  for (const n of comp.body) {
684
1301
  if (isControlGate(n))
685
1302
  continue;
686
- // coverage probes under-typed components too; an unresolvable node just isn't a prior-node cell.
687
1303
  try {
688
1304
  typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
689
1305
  }
@@ -693,6 +1309,26 @@ function buildTypedNodes(comp, plan) {
693
1309
  }
694
1310
  return typedNodes;
695
1311
  }
1312
+ /** mapAsBinding — the `$as` element binding for a map node's element ports, or undefined (uncovered). */
1313
+ function mapAsBinding(comp, node, typedNodes, plan) {
1314
+ try {
1315
+ return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
1316
+ }
1317
+ catch {
1318
+ return undefined;
1319
+ }
1320
+ }
1321
+ /** portIsStatic — the ELIGIBILITY gate ASKS the one port lowering (compilePortNode / native-expr) whether
1322
+ * it can lower this port. A port that does not lower (or lowers fallibly) fails closed. */
1323
+ function portIsStatic(node, comp, plan, typedNodes, asBinding) {
1324
+ try {
1325
+ compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
1326
+ return true;
1327
+ }
1328
+ catch {
1329
+ return false;
1330
+ }
1331
+ }
696
1332
  /** The output must lower to a typed struct/value with no scope read: a ref to a BODY NODE, an
697
1333
  * obj/arr of such, or a concat. A ref to an input param / non-concat operator is not lowerable. */
698
1334
  function outputIsNativeLowerable(comp) {
@@ -724,7 +1360,7 @@ function outputIsNativeLowerable(comp) {
724
1360
  /**
725
1361
  * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet), rust twin of the go predicate.
726
1362
  */
727
- function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
1363
+ function coveredMapNode(n, bodyIds, comp) {
728
1364
  if (n === null || typeof n !== "object" || !("map" in n))
729
1365
  return false;
730
1366
  const m = n.map;
@@ -756,25 +1392,17 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
756
1392
  return false;
757
1393
  }
758
1394
  const ports = m.ports;
1395
+ const plan = planForComp(comp);
1396
+ const typedNodes = buildTypedNodes(comp, plan);
759
1397
  const asBinding = mapAsBinding(comp, n, typedNodes, plan);
760
1398
  for (const p of Object.keys(ports))
761
1399
  if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
762
1400
  return false;
763
1401
  return true;
764
1402
  }
765
- /** the `$as` element binding for a covered map/fanout node's element ports (name + over-element TypeRef),
766
- * or undefined when the over element type does not resolve (the node is then uncovered upstream). */
767
- function mapAsBinding(comp, node, typedNodes, plan) {
768
- try {
769
- return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
770
- }
771
- catch {
772
- return undefined;
773
- }
774
- }
775
1403
  /** coveredFanoutNode (v3, rust twin) — a first-class fanout node is covered when its over is a covered
776
1404
  * ref (prior-node arr / input array with elemType) and its element ports are static. See the go twin. */
777
- function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
1405
+ function coveredFanoutNode(n, bodyIds, comp) {
778
1406
  if (n === null || typeof n !== "object" || !("fanout" in n))
779
1407
  return false;
780
1408
  const f = n.fanout;
@@ -794,6 +1422,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
794
1422
  return false;
795
1423
  }
796
1424
  const ports = f.ports;
1425
+ const plan = planForComp(comp);
1426
+ const typedNodes = buildTypedNodes(comp, plan);
797
1427
  let asBinding;
798
1428
  try {
799
1429
  asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
@@ -838,8 +1468,8 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
838
1468
  }
839
1469
  /** A covered read SHAPE with static ports + native-lowerable output (+ covered map...into), OR a
840
1470
  * real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point reads. */
841
- function isSequentialComponentRefRead(comp, plan) {
842
- return coverageRejectReason(comp, plan) === null;
1471
+ function isSequentialComponentRefRead(comp) {
1472
+ return coverageRejectReason(comp) === null;
843
1473
  }
844
1474
  /**
845
1475
  * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89), rust twin of
@@ -848,11 +1478,12 @@ function isSequentialComponentRefRead(comp, plan) {
848
1478
  * IDENTICAL to isSequentialComponentRefRead (defined as `coverageRejectReason(c) === null`) so the two
849
1479
  * can never disagree. This is the message the fail-closed dispatch surfaces per non-native component.
850
1480
  */
851
- function coverageRejectReason(comp, plan) {
1481
+ function coverageRejectReason(comp) {
1482
+ const crPlan = planForComp(comp);
1483
+ const crTypedNodes = buildTypedNodes(comp, crPlan);
852
1484
  const ep = execPlan(comp);
853
1485
  if (ep === null)
854
1486
  return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
855
- const typedNodes = buildTypedNodes(comp, plan);
856
1487
  // #114: a bindField relation child (single|connection) IS covered inside a parallel stage — the
857
1488
  // sibling hasMany fan-out (parent → members ∥ permissions). Its bound-key skip-gate is preflighted
858
1489
  // per member (from the settled parent) BEFORE dispatch, so the scoped-thread fan-out keeps
@@ -890,12 +1521,12 @@ function coverageRejectReason(comp, plan) {
890
1521
  continue;
891
1522
  }
892
1523
  if ("fanout" in n) {
893
- if (!coveredFanoutNode(n, bodyIds, comp, plan, typedNodes))
1524
+ if (!coveredFanoutNode(n, bodyIds, comp))
894
1525
  return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
895
1526
  continue;
896
1527
  }
897
1528
  if ("map" in n) {
898
- if (!coveredMapNode(n, bodyIds, comp, plan, typedNodes))
1529
+ if (!coveredMapNode(n, bodyIds, comp))
899
1530
  return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
900
1531
  continue;
901
1532
  }
@@ -914,7 +1545,7 @@ function coverageRejectReason(comp, plan) {
914
1545
  if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
915
1546
  return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
916
1547
  for (const p of Object.keys(cr.ports))
917
- if (portIsStatic(cr.ports[p], comp, plan, typedNodes) === false)
1548
+ if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
918
1549
  return `node '${n.id}' port '${p}' is not statically resolvable`;
919
1550
  }
920
1551
  if (!outputIsNativeLowerable(comp))
@@ -931,42 +1562,28 @@ function isFullyTyped(comp) {
931
1562
  return false;
932
1563
  return true;
933
1564
  }
934
- function isNativeRaw(comp, plan) {
935
- return isSequentialComponentRefRead(comp, plan) && isFullyTyped(comp);
1565
+ function isNativeRaw(comp) {
1566
+ return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
936
1567
  }
937
- function assertTypedCoverage(comp, plan) {
938
- if (!isSequentialComponentRefRead(comp, plan))
1568
+ function assertTypedCoverage(comp) {
1569
+ if (!isSequentialComponentRefRead(comp))
939
1570
  return;
940
1571
  if (isFullyTyped(comp))
941
1572
  return;
942
1573
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on rust-straightline-native (boxed result).`);
943
1574
  }
944
1575
  // ── typed input struct (InNR<Comp>) + NATIVE scalar port lowering ────────────────────────────
945
- /**
946
- * inputPortRustType — the native Rust type for an input port's declared type. Scalars lower to the
947
- * concrete scalar; `array`/`map` with a declared elemType to `Vec<ElemT>` / `BTreeMap<String, ElemT>`;
948
- * an OPTIONAL port (`{opt:T}` → `required:false`) to `Option<T>` — the native representation of "the
949
- * value is absent". A declared type with no native lowering stays the boxed `Value`.
950
- *
951
- * An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: `Option<Value>` would be a
952
- * boxed escape, and emitting the bare inner type would silently turn "absent" into a zero value. A port
953
- * whose optionality cannot be represented is never silently coerced.
954
- */
1576
+ /** Rust type for an input port's declared type (scalar → concrete; array+elemType → Vec<ElemT>; else Value). */
955
1577
  function inputPortRustType(schema, plan, where = "input port") {
956
1578
  const ref = inputPortTypeRef(schema, plan);
957
1579
  if (ref !== undefined)
958
1580
  return renderTypeRef(ref);
959
1581
  if (inputPortIsOptional(schema))
960
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Rust lowering, so "absent" has no native representation (an Option<Value> would be a boxed escape, and the bare inner type would silently read absent as a zero value). Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
1582
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Rust lowering, so "absent" has no native representation. Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
961
1583
  return "Value";
962
1584
  }
963
- /** The scalar kind a REQUIRED input port declares, or undefined when it is not a required native scalar.
964
- * An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `Option<T>`, and the
965
- * callers of this function build REQUIRED scalar slots (`in_.<field>` read straight into a non-opt port
966
- * field). Answering "i64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
1585
+ /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
967
1586
  function inputScalarKind(schema) {
968
- if (inputPortIsOptional(schema))
969
- return undefined;
970
1587
  switch (schema?.type) {
971
1588
  case "string":
972
1589
  case "literal": // a `"literal"` union is a constrained String (see inputPortRustType).
@@ -990,6 +1607,8 @@ function emitInStruct(comp, plan) {
990
1607
  if (ports.length === 0) {
991
1608
  return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\n#[derive(Default)]\npub struct ${name};`;
992
1609
  }
1610
+ // #139: inputPortRustType reads the inputPortTypeRef SSoT — an OPTIONAL port is `Option<T>` (absent →
1611
+ // None); an optional whose inner type has no native lowering FAILS CLOSED (never a boxed escape).
993
1612
  const fields = ports
994
1613
  .map((p) => ` pub ${rustFieldName(p)}: ${inputPortRustType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`)}, // ${JSON.stringify(p)}`)
995
1614
  .join("\n");
@@ -1000,18 +1619,30 @@ pub struct ${name} {
1000
1619
  ${fields}
1001
1620
  }`;
1002
1621
  }
1003
- /** The port forms the STATIC (non-optional) lane lowers named in its reject message so the message
1004
- * describes what is actually covered rather than a stale corpus list. */
1622
+ /** A ref resolver for native lowering: given a ref head + remaining field path, return a native scalar
1623
+ * Rust expression (typed input field / prior-node typed struct scalar / element binding field), or null
1624
+ * when the ref does not resolve to a REQUIRED (non-opt) native scalar (then the caller falls back to the
1625
+ * Value path). Returned exprs are OWNED (String cloned / scalar copied) — usable in a `format!` / literal. */
1626
+ // bc#90 / runtime-free: the old `emitStaticR` / `inputLeafR` / `valueLeafR` port lowering (which built
1627
+ // `Value` leaves via the primitives::* helpers and struct-field serializers) is REMOVED — every covered
1628
+ // port now lowers to a FULLY NATIVE Rust value in emitPortInit (string / bool / static-string-array /
1629
+ // number literal), and a genuinely-dynamic port fails closed. There is no boxed-Value fallback on the
1630
+ // covered read plane.
1005
1631
  /**
1006
- * emitPortInit — the port field initializer, read off the ONE port lowering (compilePortNode / native-
1007
- * expr). The field's declared TYPE (portFieldRustType) is the SAME lowering's TypeRef, so type and value
1008
- * can never disagree. `isPrior` tests whether a ref head names a settled prior node at this position;
1009
- * `opts.asBinding`/`opts.asBase` carry a map/fanout element binding.
1632
+ * emitPortInit — emit the port field build for one port as a FULLY NATIVE Rust expression (bc#90 /
1633
+ * runtime-free). Every covered port lowers to a concrete Rust value assigned straight into the ports
1634
+ * struct field NO `Value`, NO fallback path, NO `match`:
1635
+ * - a static string array (projection) → `vec!["a", "b", ...]` (change #2);
1636
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `20i64` / `<n>f64`;
1637
+ * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
1638
+ * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered plane —
1639
+ * a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
1640
+ * ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
1010
1641
  */
1011
1642
  function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
1012
1643
  const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
1013
1644
  if (c.stmts.length > 0)
1014
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (one that can raise INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS). Such an expression lowers to STATEMENTS carrying an early error-return, and a port is built inline in the ports-struct literal — a position that cannot host them. (A cond/guard position CAN host them, so the same expression is covered there — the boundary is the POSITION, not the operator.) Use a non-fallible port expression.`);
1645
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): port '${pn}' ${JSON.stringify(expr)} lowers to a FALLIBLE expression (INT_OVERFLOW / NAN_OR_INF / MOD_ZERO / PRECISION_LOSS) statements with an early error-return, which a ports-struct literal cannot host. Use a non-fallible port expression.`);
1015
1646
  return `${portFieldName(pn)}: ${c.expr}`;
1016
1647
  }
1017
1648
  // ── the combined runner (STRUCT-returning; INLINE sequential; native ports in; concrete row out) ──
@@ -1216,22 +1847,57 @@ function emitNativeRawRunner(comp, plan, ap) {
1216
1847
  // directly (no `.await`). The runner being `async fn` does not force every call site to await.
1217
1848
  const awaitSfx = ap.nodeIsAsync(node.id) ? ".await" : "";
1218
1849
  lines.push(`${indent}let ports_${s} = ${structName} { ${inits.join(", ")} };`);
1219
- lines.push(`${indent}let row_${s} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1220
- lines.push(`${indent} Some(r) => r,`);
1221
- lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(node.component)})),`);
1222
- lines.push(`${indent}};`);
1850
+ // a de-box-eligible node returns the wire (Self::Wire / Vec / Option / BTreeMap); the generated
1851
+ // decode_wire_result_* probes each record strictly and produces the structured error on a mismatch
1852
+ // (byte-equal codes to run_behavior's outType check, so ≡ holds). A non-de-box node keeps its direct
1853
+ // concrete-row move.
1854
+ const top = recordTop(ref);
1855
+ const namedTop = top !== null && top.kind === "named";
1856
+ const deBox = top !== null;
1223
1857
  if (meta.policy === "continue") {
1224
- lines.push(`${indent}if !row_${s}.is_error {`);
1225
- lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1226
- lines.push(`${indent} produced_${s}.set(true);`);
1227
- lines.push(`${indent}}`);
1858
+ // continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream
1859
+ // Skip propagation is what `continue` means. Only a clean decode commits the result.
1860
+ if (namedTop) {
1861
+ lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1862
+ lines.push(`${indent} if let Ok(row_${s}) = ${rustDecodeNamedName(top.innerName)}(&wire_${s}) {`);
1863
+ lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1864
+ lines.push(`${indent} produced_${s}.set(true);`);
1865
+ lines.push(`${indent} }`);
1866
+ lines.push(`${indent}}`);
1867
+ }
1868
+ else if (deBox) {
1869
+ lines.push(`${indent}if let Ok(wire_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1870
+ lines.push(`${indent} if let Ok(res_${s}) = ${rustDecodeResultName(comp.name, node.id)}(wire_${s}) {`);
1871
+ lines.push(`${indent} *${typedCell(node.id)}.borrow_mut() = res_${s};`);
1872
+ lines.push(`${indent} produced_${s}.set(true);`);
1873
+ lines.push(`${indent} }`);
1874
+ lines.push(`${indent}}`);
1875
+ }
1876
+ else {
1877
+ lines.push(`${indent}if let Ok(row_${s}) = handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1878
+ lines.push(`${indent} ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1879
+ lines.push(`${indent} produced_${s}.set(true);`);
1880
+ lines.push(`${indent}}`);
1881
+ }
1228
1882
  }
1229
1883
  else {
1230
- const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1231
- lines.push(`${indent}if row_${s}.is_error {`);
1232
- lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1233
- lines.push(`${indent}}`);
1234
- lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1884
+ const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1885
+ const resultVar = deBox ? `wire_${s}` : `row_${s}`;
1886
+ lines.push(`${indent}let ${resultVar} = match handlers.${nodeMethodName(node.id)}(&ports_${s}, ${boundArg})${awaitSfx} {`);
1887
+ lines.push(`${indent} Ok(r) => r,`);
1888
+ lines.push(`${indent} Err(e) => return Err(BehaviorError { code: "OP_FAILED".to_string(), message: format!("operation '{}' failed under '${policyLit}: {}", ${rustStrLit(node.id)}, e.message), detail: e.detail }),`);
1889
+ lines.push(`${indent}};`);
1890
+ if (namedTop) {
1891
+ lines.push(`${indent}let row_${s} = ${rustDecodeNamedName(top.innerName)}(&wire_${s})?;`);
1892
+ lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1893
+ }
1894
+ else if (deBox) {
1895
+ lines.push(`${indent}let res_${s} = ${rustDecodeResultName(comp.name, node.id)}(wire_${s})?;`);
1896
+ lines.push(`${indent}*${typedCell(node.id)}.borrow_mut() = res_${s};`);
1897
+ }
1898
+ else {
1899
+ lines.push(`${indent}${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1900
+ }
1235
1901
  lines.push(`${indent}produced_${s}.set(true);`);
1236
1902
  }
1237
1903
  for (const c of closers)
@@ -1356,10 +2022,26 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1356
2022
  const nMembers = refMembers.length;
1357
2023
  const boundN = Math.min(concurrency, nMembers);
1358
2024
  const jobsVar = `jobs_${sanitize(comp.name)}`;
2025
+ const hWire = `<H as ${handlerTraitName(comp.name)}>::Wire`;
2026
+ const hWireTop = (top) => {
2027
+ switch (top.kind) {
2028
+ case "named":
2029
+ return hWire;
2030
+ case "arr":
2031
+ return `Vec<${hWire}>`;
2032
+ case "opt":
2033
+ return `Option<${hWire}>`;
2034
+ case "map":
2035
+ return `std::collections::BTreeMap<String, ${hWire}>`;
2036
+ }
2037
+ };
1359
2038
  for (const i of refMembers) {
1360
2039
  const s = sanitize(comp.body[i].id);
1361
- const rowT = rawRowStructName(comp.name, comp.body[i].id);
1362
- lines.push(` let slot_${s}: std::sync::Mutex<Option<Option<${rowT}>>> = std::sync::Mutex::new(None);`);
2040
+ // a de-box member's thread returns the WIRE (shaped by the record top); a non-de-box member returns
2041
+ // its concrete row directly.
2042
+ const memTop = recordTop(typedNodes.get(comp.body[i].id));
2043
+ const slotT = memTop !== null ? hWireTop(memTop) : rawRowStructName(comp.name, comp.body[i].id);
2044
+ lines.push(` let slot_${s}: std::sync::Mutex<Option<Result<${slotT}, BehaviorError>>> = std::sync::Mutex::new(None);`);
1363
2045
  }
1364
2046
  lines.push(` let mut ${jobsVar}: Vec<usize> = Vec::new();`);
1365
2047
  refMembers.forEach((i, slot) => {
@@ -1397,22 +2079,52 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1397
2079
  const s = sanitize(node.id);
1398
2080
  const ref = typedNodes.get(node.id);
1399
2081
  lines.push(` if ports_${s}.is_some() {`);
1400
- lines.push(` let row_${s} = match slot_${s}.lock().unwrap().take() {`);
1401
- lines.push(` Some(Some(r)) => r,`);
1402
- lines.push(` _ => return Err(unknown_component(${rustStrLit(node.component)})),`);
1403
- lines.push(` };`);
2082
+ lines.push(` let out_${s} = slot_${s}.lock().unwrap().take().expect("runnable member wrote its slot");`);
2083
+ const top = recordTop(ref);
2084
+ const namedTop = top !== null && top.kind === "named";
2085
+ const deBox = top !== null;
1404
2086
  if (meta.policy === "continue") {
1405
- lines.push(` if !row_${s}.is_error {`);
1406
- lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
1407
- lines.push(` produced_${s}.set(true);`);
1408
- lines.push(` }`);
2087
+ if (namedTop) {
2088
+ lines.push(` if let Ok(wire_${s}) = out_${s} {`);
2089
+ lines.push(` if let Ok(row_${s}) = ${rustDecodeNamedName(top.innerName)}(&wire_${s}) {`);
2090
+ lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2091
+ lines.push(` produced_${s}.set(true);`);
2092
+ lines.push(` }`);
2093
+ lines.push(` }`);
2094
+ }
2095
+ else if (deBox) {
2096
+ lines.push(` if let Ok(wire_${s}) = out_${s} {`);
2097
+ lines.push(` if let Ok(res_${s}) = ${rustDecodeResultName(comp.name, node.id)}(wire_${s}) {`);
2098
+ lines.push(` *${typedCell(node.id)}.borrow_mut() = res_${s};`);
2099
+ lines.push(` produced_${s}.set(true);`);
2100
+ lines.push(` }`);
2101
+ lines.push(` }`);
2102
+ }
2103
+ else {
2104
+ lines.push(` if let Ok(row_${s}) = out_${s} {`);
2105
+ lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2106
+ lines.push(` produced_${s}.set(true);`);
2107
+ lines.push(` }`);
2108
+ }
1409
2109
  }
1410
2110
  else {
1411
- const label = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1412
- lines.push(` if row_${s}.is_error {`);
1413
- lines.push(` return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under '${label}: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1414
- lines.push(` }`);
1415
- lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2111
+ const policyLit = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
2112
+ const resultVar = deBox ? `wire_${s}` : `row_${s}`;
2113
+ lines.push(` let ${resultVar} = match out_${s} {`);
2114
+ lines.push(` Ok(r) => r,`);
2115
+ lines.push(` Err(e) => return Err(BehaviorError { code: "OP_FAILED".to_string(), message: format!("operation '{}' failed under '${policyLit}: {}", ${rustStrLit(node.id)}, e.message), detail: e.detail }),`);
2116
+ lines.push(` };`);
2117
+ if (namedTop) {
2118
+ lines.push(` let row_${s} = ${rustDecodeNamedName(top.innerName)}(&wire_${s})?;`);
2119
+ lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2120
+ }
2121
+ else if (deBox) {
2122
+ lines.push(` let res_${s} = ${rustDecodeResultName(comp.name, node.id)}(wire_${s})?;`);
2123
+ lines.push(` *${typedCell(node.id)}.borrow_mut() = res_${s};`);
2124
+ }
2125
+ else {
2126
+ lines.push(` ${emitRowMove(`*${typedCell(node.id)}.borrow_mut()`, `row_${s}`, ref, plan)}`);
2127
+ }
1416
2128
  lines.push(` produced_${s}.set(true);`);
1417
2129
  }
1418
2130
  lines.push(` }`);
@@ -1449,18 +2161,28 @@ isAsync = false) {
1449
2161
  const portNames = Object.keys(m.ports);
1450
2162
  const elemArrRef = typedNodes.get(node.id);
1451
2163
  const elemName = renderTypeRef(elemArrRef.elem);
1452
- const asBinding = { name: asName, ref: overElemRef, plan };
2164
+ // a record element is de-boxed strictly: the handler returns the element WIRE(s) and the arm decodes via
2165
+ // decode_wire_elem_* (closing the scan-row hole). A de-box mismatch fails closed regardless of
2166
+ // elementPolicy (matching the interpreter's whole-node outType throw).
2167
+ const elemDeBox = mapFanoutElemDeBoxEligible(node, typedNodes, plan);
2168
+ const decElemFn = elemDeBox ? rustDecodeElemName(comp.name, node.id) : "";
1453
2169
  // build the per-element CONCRETE native ports struct. `il` = indent inside the element loop.
1454
2170
  const buildPorts = (il) => {
1455
- const inits = portNames.map((pn) => emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
1456
- return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
2171
+ const out = [];
2172
+ const inits = [];
2173
+ const asBinding = { name: asName, ref: overElemRef, plan };
2174
+ for (const pn of portNames) {
2175
+ inits.push(emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
2176
+ }
2177
+ out.push(`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`);
2178
+ return out;
1457
2179
  };
1458
2180
  // #108 guard: compile `when` to a native bool over an element-scoped resolveHead ($as element value,
1459
2181
  // prior node, input port). On skip: `continue` (element excluded — matches run_behavior's keep-gate).
1460
2182
  const emitGuard = (il) => {
1461
2183
  if (!hasGuard)
1462
2184
  return [];
1463
- const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` });
2185
+ const resolveHead = nativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: `oel_${s}` });
1464
2186
  const g = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compileBool(m.when);
1465
2187
  const out = [];
1466
2188
  for (const st of g.stmts)
@@ -1500,26 +2222,30 @@ isAsync = false) {
1500
2222
  lines.push(`${indent} let want_${s} = items_${s}.len();`);
1501
2223
  lines.push(`${indent} let bp_${s} = ${batch} { items: items_${s} };`);
1502
2224
  lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
1503
- lines.push(`${indent} Some(r) => r,`);
1504
- lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
2225
+ lines.push(`${indent} Ok(r) => r,`);
2226
+ lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
1505
2227
  lines.push(`${indent} };`);
1506
- lines.push(`${indent} if row_${s}.is_error {`);
1507
- lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1508
- lines.push(`${indent} }`);
1509
- lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
2228
+ const batchLen = elemDeBox ? `row_${s}.len()` : `row_${s}.rows.len()`;
2229
+ const batchIter = elemDeBox ? `row_${s}.into_iter()` : `row_${s}.rows.into_iter()`;
2230
+ lines.push(`${indent} if ${batchLen} != want_${s} {`);
1510
2231
  lines.push(`${indent} return Err(BehaviorError::new("MAP_BATCH_RESULT_MISMATCH", format!("map '{}': batched handler must return a list aligned to items (want {})", ${rustStrLit(node.id)}, want_${s})));`);
1511
2232
  lines.push(`${indent} }`);
1512
2233
  if (hasInto) {
1513
- // zip the aligned batch rows onto each KEPT over element (into materializer).
1514
- lines.push(`${indent} let mut rows_iter_${s} = row_${s}.rows.into_iter();`);
2234
+ // zip the aligned batch rows onto each KEPT over element (into materializer). A record element's
2235
+ // wire is de-boxed strictly first.
2236
+ lines.push(`${indent} let mut rows_iter_${s} = ${batchIter};`);
1515
2237
  lines.push(`${indent} for over_el_${s} in kept_${s}.iter() {`);
1516
2238
  lines.push(`${indent} let er_${s} = rows_iter_${s}.next().expect("aligned batch row");`);
2239
+ if (elemDeBox)
2240
+ lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
1517
2241
  lines.push(`${indent} built_${s}.push(${elemMat}(over_el_${s}, er_${s}));`);
1518
2242
  lines.push(`${indent} }`);
1519
2243
  }
1520
2244
  else {
1521
2245
  // no-into: collect each element DIRECTLY from the aligned per-element row (result = kept rows).
1522
- lines.push(`${indent} for er_${s} in row_${s}.rows.into_iter() {`);
2246
+ lines.push(`${indent} for er_${s} in ${batchIter} {`);
2247
+ if (elemDeBox)
2248
+ lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
1523
2249
  lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemArrRef.elem, plan)}`);
1524
2250
  lines.push(`${indent} built_${s}.push(el_${s});`);
1525
2251
  lines.push(`${indent} }`);
@@ -1533,13 +2259,21 @@ isAsync = false) {
1533
2259
  lines.push(l);
1534
2260
  for (const l of buildPorts(indent + " "))
1535
2261
  lines.push(l);
2262
+ // Element Error Policy (scp-error.md): `skip` drops the failing element and proceeds (order is
2263
+ // preserved; the leaf keeps the Error Value it produced). `error` (default) promotes the element
2264
+ // Failure to the map's Component Failure, carrying the leaf's detail through.
1536
2265
  lines.push(`${indent} let er_${s} = match handlers.${nodeMethodName(node.id)}(&ep_${s}, None)${awaitSfx} {`);
1537
- lines.push(`${indent} Some(r) => r,`);
1538
- lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(m.component)})),`);
2266
+ lines.push(`${indent} Ok(r) => r,`);
2267
+ if (m.elementPolicy === "skip") {
2268
+ lines.push(`${indent} Err(_) => continue,`);
2269
+ }
2270
+ else {
2271
+ lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
2272
+ }
1539
2273
  lines.push(`${indent} };`);
1540
- lines.push(`${indent} if er_${s}.is_error {`);
1541
- lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, er_${s}.err)));`);
1542
- lines.push(`${indent} }`);
2274
+ // a record element's wire is de-boxed strictly (fail-closed regardless of elementPolicy).
2275
+ if (elemDeBox)
2276
+ lines.push(`${indent} let er_${s} = ${decElemFn}(&er_${s})?;`);
1543
2277
  if (hasInto) {
1544
2278
  lines.push(`${indent} built_${s}.push(${elemMat}(oel_${s}, er_${s}));`);
1545
2279
  }
@@ -1583,9 +2317,12 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
1583
2317
  if (!dkField || dkField.type.kind !== "scalar" || dkField.type.scalar !== "string")
1584
2318
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust fanout (v3): node '${node.id}' dedupeKey '${f.dedupeKey}' must be a string field of the connection items element type '${elemRef.name}'.`);
1585
2319
  const dkRust = rustFieldName(f.dedupeKey);
1586
- const asBinding = { name: asName, ref: overElemRef, plan };
1587
2320
  const buildPorts = (il) => {
1588
- const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
2321
+ const inits = [];
2322
+ const asBinding = { name: asName, ref: overElemRef, plan };
2323
+ for (const pn of portNames) {
2324
+ inits.push(emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: `oel_${s}` }));
2325
+ }
1589
2326
  return [`${il}let ep_${s} = ${structName} { ${inits.join(", ")} };`];
1590
2327
  };
1591
2328
  const lines = [];
@@ -1609,20 +2346,26 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
1609
2346
  lines.push(`${indent} let want_${s} = pi_${s}.len();`);
1610
2347
  lines.push(`${indent} let bp_${s} = ${batch} { items: pi_${s} };`);
1611
2348
  lines.push(`${indent} let row_${s} = match handlers.${nodeMethodName(node.id)}(&bp_${s}, None)${awaitSfx} {`);
1612
- lines.push(`${indent} Some(r) => r,`);
1613
- lines.push(`${indent} None => return Err(unknown_component(${rustStrLit(f.component)})),`);
2349
+ lines.push(`${indent} Ok(r) => r,`);
2350
+ lines.push(`${indent} Err(e) => return Err(op_failed(${rustStrLit(node.id)}, "fail", e)),`);
1614
2351
  lines.push(`${indent} };`);
1615
- lines.push(`${indent} if row_${s}.is_error {`);
1616
- lines.push(`${indent} return Err(BehaviorError::new("OP_FAILED", format!("operation '{}' failed under 'fail' policy: {}", ${rustStrLit(node.id)}, row_${s}.err)));`);
1617
- lines.push(`${indent} }`);
1618
- lines.push(`${indent} if row_${s}.rows.len() != want_${s} {`);
2352
+ // the fanout element row is a named record → the handler returns the aligned element WIRES
2353
+ // (Vec<Option<Self::Wire>>) and each present body is de-boxed strictly (a de-box mismatch fails closed).
2354
+ // A DANGLING id is None → the zero element row (empty dedupeKey), which the dangling drop excludes.
2355
+ const decElemFn = rustDecodeElemName(comp.name, node.id);
2356
+ const rawElemT = rawElemStructName(comp.name, node.id);
2357
+ lines.push(`${indent} if row_${s}.len() != want_${s} {`);
1619
2358
  lines.push(`${indent} return Err(BehaviorError::new("FANOUT_BATCH_RESULT_MISMATCH", format!("fanout '{}': batched handler must return a list aligned to the deduped id list (want {})", ${rustStrLit(node.id)}, want_${s})));`);
1620
2359
  lines.push(`${indent} }`);
1621
2360
  // ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
1622
2361
  // drop dangling (empty dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
1623
2362
  // structural (the elem type already omits it). cursor is always None (connection wrap).
1624
2363
  lines.push(`${indent} let mut seen_${s}: std::collections::HashSet<String> = std::collections::HashSet::new();`);
1625
- lines.push(`${indent} for er_${s} in row_${s}.rows.into_iter() {`);
2364
+ lines.push(`${indent} for opt_er_${s} in row_${s}.into_iter() {`);
2365
+ lines.push(`${indent} let er_${s}: ${rawElemT} = match opt_er_${s} {`);
2366
+ lines.push(`${indent} Some(wire_${s}) => ${decElemFn}(&wire_${s})?,`);
2367
+ lines.push(`${indent} None => Default::default(),`);
2368
+ lines.push(`${indent} };`);
1626
2369
  lines.push(`${indent} ${emitElemMove(`let el_${s}`, `er_${s}`, elemRef, plan)}`);
1627
2370
  lines.push(`${indent} let key_${s} = el_${s}.${dkRust}.clone();`);
1628
2371
  if (f.drop === "dangling") {
@@ -1639,73 +2382,6 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, isAsync
1639
2382
  lines.push(c);
1640
2383
  return lines.join("\n");
1641
2384
  }
1642
- /**
1643
- * rustOwn — render the OWNED form of a resolved ref value at the LEAF (native-expr calls this after the
1644
- * field path is walked). `guard` marks a value read through a `RefCell`/`&` borrow (a prior node's
1645
- * `cell.borrow()`, a `$as` element `&Elem`): a WHOLE-node Copy scalar dereferences (`*expr`), while a
1646
- * Copy FIELD is already a copy-out of the auto-deref'd place. A String/struct/map clones; an array clones
1647
- * only when the position OWNS it (a port field) and stays borrowed for a `len`. Cloning a Copy value
1648
- * would trip clippy's `clone_on_copy`, so Copy never clones.
1649
- */
1650
- function rustOwn(guard, ownArrays) {
1651
- return (expr, ref, fieldAccessed) => {
1652
- if (ref.kind === "arr")
1653
- return ownArrays ? `${expr}.clone()` : expr;
1654
- if (typeRefIsCopy(ref))
1655
- return guard && !fieldAccessed ? `*${expr}` : expr;
1656
- return `${expr}.clone()`;
1657
- };
1658
- }
1659
- /**
1660
- * nativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
1661
- * `if`/branches, a map/fanout `when` guard, AND a port. A head is a `$as` element binding, a prior body
1662
- * node's typed cell, or an input port — each a native base expr of the head's TypeRef PLUS an `own`
1663
- * renderer (rustOwn) that native-expr applies at the resolved LEAF. Ownership lives at the leaf so a
1664
- * prior-node field ref clones only the field (`cell.borrow().items.clone()`), never the whole struct.
1665
- * `isPrior` tests whether a head names a settled prior node (position-aware at a runner site; structural
1666
- * at coverage/struct-def). `asBase` is the element base for a `$as`-headed ref; `ownArrays` owns an array
1667
- * (a port field) vs borrows it (a cond `len`).
1668
- */
1669
- function nativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
1670
- const ownArrays = opts?.ownArrays === true;
1671
- return (head) => {
1672
- // a `$as` element (`for oel in vec.iter()`) is a `&Elem` — a borrow guard.
1673
- if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
1674
- return { expr: opts.asBase, ref: opts.asBinding.ref, own: rustOwn(true, ownArrays) };
1675
- }
1676
- // a prior node's value lives behind a `RefCell` borrow guard.
1677
- if (isPrior(head)) {
1678
- const ref = typedNodes.get(head);
1679
- return { expr: `${typedCell(head)}.borrow()`, ref, own: rustOwn(true, ownArrays) };
1680
- }
1681
- // an input port is a by-value struct field (no guard).
1682
- const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
1683
- if (ref === undefined)
1684
- return null;
1685
- return { expr: `in_.${rustFieldName(head)}`, ref, own: rustOwn(false, ownArrays) };
1686
- };
1687
- }
1688
- /**
1689
- * compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
1690
- * native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
1691
- * `ref`, its initializer the compiled `expr` — so the two are derived from a SINGLE lowering and can
1692
- * never disagree. Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array,
1693
- * `$as` element field), a concat, a static array, an optional-input read — all resolve here. A port that
1694
- * cannot lower to a native value FAILS CLOSED loudly. A lowering that is FALLIBLE (hoists statements) is
1695
- * a covered SHAPE returned here as-is; only the value site (emitPortInit) rejects it, since a ports-struct
1696
- * literal cannot host statements — the coverage gate/type site must still see it as covered.
1697
- */
1698
- function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
1699
- const resolveHead = nativeResolveHead(comp, plan, typedNodes, isPrior, { ...opts, ownArrays: true });
1700
- try {
1701
- return new NativeExprCompiler(makeRustExprBackend(resolveHead, plan)).compilePort(node);
1702
- }
1703
- catch (e) {
1704
- if (!(e instanceof GeneratorFailure))
1705
- throw e;
1706
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Rust value: ${e.message}`);
1707
- }
1708
- }
1709
2385
  /**
1710
2386
  * emitCondArm — the struct-native exec of a covered cond node (#108, rust). Mirrors run_behavior's cond
1711
2387
  * lower: compile `if` to a native bool, materialize the TAKEN branch to the node's outType (only the
@@ -1847,6 +2523,13 @@ pub const COMPONENT_NAMES_NATIVE_RAW: [&str; ${names.length}] = [${names.join(",
1847
2523
  const UNKNOWN_COMPONENT_HELPER = `#[allow(dead_code)]
1848
2524
  fn unknown_component(component: &str) -> BehaviorError {
1849
2525
  BehaviorError::new("UNKNOWN_COMPONENT", format!("component '{component}' has no handler (fail-closed)"))
2526
+ }
2527
+
2528
+ // leaf_failure — a failure the LEAF reports (test glue): the runner assigns the node's failure code
2529
+ // from its Error Policy Kind, so what a leaf carries is its message and its structured Error Value.
2530
+ #[allow(dead_code)]
2531
+ fn leaf_failure(message: impl Into<String>) -> BehaviorError {
2532
+ BehaviorError::new("LEAF_FAILURE", message)
1850
2533
  }`;
1851
2534
  // ── module assembly ──────────────────────────────────────────────────────────────────
1852
2535
  function emit(ctx) {
@@ -1855,20 +2538,17 @@ function emit(ctx) {
1855
2538
  // rust CONSUMES this annotation (async fn + .await where derived); go ignores it. per-node granularity
1856
2539
  // is a strict generalization of increment-1's uniform ioModel:'async' (= every terminal async).
1857
2540
  const asyncPlans = buildAsyncPlans(ctx.ir, ctx.ioModel);
1858
- // The type plan is the SSoT the coverage gate + the port lowering both read (the gate ASKS the port
1859
- // lowering whether a port lowers), so it is built up-front — before the coverage checks.
1860
- const plan = buildTypePlan(ctx.ir);
1861
2541
  for (const c of ctx.ir.components)
1862
- assertTypedCoverage(c, plan);
1863
- const native = ctx.ir.components.filter((c) => isNativeRaw(c, plan));
1864
- const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c, plan));
2542
+ assertTypedCoverage(c);
2543
+ const native = ctx.ir.components.filter(isNativeRaw);
2544
+ const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
1865
2545
  // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. Previously a
1866
2546
  // non-covered component was SILENTLY DROPPED (emitted only as a comment note), concealing that it
1867
2547
  // never got native codegen. We now throw a LOUD GeneratorFailure naming every uncovered component +
1868
2548
  // its specific reject reason. A fully-covered module is unchanged.
1869
2549
  if (nonNative.length > 0) {
1870
2550
  const rejects = nonNative
1871
- .map((c) => ` - component '${c.name}': ${coverageRejectReason(c, plan) ?? "not a covered native read"}`)
2551
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
1872
2552
  .join("\n");
1873
2553
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `rust typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
1874
2554
  `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
@@ -1882,6 +2562,7 @@ function emit(ctx) {
1882
2562
  ${emitStructSurface([])}
1883
2563
  `;
1884
2564
  }
2565
+ const plan = buildTypePlan(ctx.ir);
1885
2566
  // The typed-native module is a de-boxed CONSUMER artifact: the covered runner RETURNS these outType
1886
2567
  // structs and the consumer reads their fields natively. So the decls (and their FIELDS) are pub — the
1887
2568
  // consumer keeps the model native (mirrors Go's exported outType struct fields on the covered path).
@@ -1898,8 +2579,15 @@ ${emitStructSurface([])}
1898
2579
  const handlerTraits = [];
1899
2580
  const inStructs = [];
1900
2581
  const elemMaterializers = [];
2582
+ // the generated strict de-box functions (deduped by named type across all covered components).
2583
+ const decodeFns = [];
2584
+ const decodeEmitted = new Set();
1901
2585
  for (const c of native) {
1902
- const typedNodes = buildTypedNodes(c, plan);
2586
+ const typedNodes = new Map();
2587
+ // build the full typed-node map FIRST so a map's over-element resolution can see every node.
2588
+ for (const n of c.body)
2589
+ if (!isControlGate(n))
2590
+ typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
1903
2591
  for (const n of c.body) {
1904
2592
  if ("fanout" in n)
1905
2593
  structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
@@ -1909,10 +2597,13 @@ ${emitStructSurface([])}
1909
2597
  // #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
1910
2598
  }
1911
2599
  else
1912
- structs.push(emitPortsStruct(c, n, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
2600
+ structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
1913
2601
  }
2602
+ // fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
2603
+ assertDeBoxableResults(c, typedNodes, plan);
1914
2604
  rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
1915
2605
  handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
2606
+ emitRustNodeDecodeFns(c, typedNodes, plan, decodeEmitted, decodeFns);
1916
2607
  inStructs.push(emitInStruct(c, plan));
1917
2608
  const mm = emitMapElemMaterializers(c, typedNodes, plan);
1918
2609
  if (mm)
@@ -1955,6 +2646,12 @@ use std::cell::RefCell;`;
1955
2646
  : undefined,
1956
2647
  `// CONCRETE per-component input structs (fields = inputPorts).\n${inStructs.join("\n\n")}`,
1957
2648
  `// CONCRETE per-component handler traits (one node_* method per node — native ports IN, row OUT).\n${handlerTraits.join("\n\n")}`,
2649
+ decodeFns.length
2650
+ ? `// strict de-box wire seam — the consumer implements WireRow/WireList over its wire payload (variant\n// classification); the generated decode_wire_* owns strictness (required/optional, present/absent, error\n// assembly, fail-closed). Concrete enums + monomorphized traits — no boxed value, no trait object, no\n// heap allocation crosses the seam.\n${emitRustProbeWireTypes()}`
2651
+ : undefined,
2652
+ decodeFns.length
2653
+ ? `// Generated strict de-box functions (per named record type; structured Error Value on mismatch).\n${decodeFns.join("\n\n")}`
2654
+ : undefined,
1958
2655
  elemMaterializers.length
1959
2656
  ? `// map augmented-element copiers (pure struct field assignment — no dynamic value read).\n${elemMaterializers.join("\n\n")}`
1960
2657
  : undefined,
@@ -1974,8 +2671,8 @@ use std::cell::RefCell;`;
1974
2671
  */
1975
2672
  export function rustTypedNativeObserve(ir, ioModel = "sync") {
1976
2673
  const components = ir.components;
2674
+ const native = components.filter(isNativeRaw);
1977
2675
  const plan = buildTypePlan(ir);
1978
- const native = components.filter((c) => isNativeRaw(c, plan));
1979
2676
  const needSync = native.some((c) => concurrencyPlan(c) !== null);
1980
2677
  const serializers = emitSerializers(plan);
1981
2678
  const marshallers = emitMarshallers(plan);
@@ -1986,6 +2683,160 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
1986
2683
  const anyAsyncRunner = native.some((c) => asyncPlans.get(c.name).runnerIsAsync);
1987
2684
  // ── per-component input decoders: generic &[(String,Value)] -> concrete InNR_<comp> (test glue). ──
1988
2685
  const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
2686
+ // does any covered read de-box a named record — a componentRef record OR a map/fanout element row
2687
+ // (→ the scripted handler returns a ScriptedWireRow / Vec<ScriptedWireRow>)?
2688
+ const anyDeBox = native.some((c) => {
2689
+ const tn = new Map();
2690
+ for (const n of c.body)
2691
+ if (!isControlGate(n))
2692
+ tn.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
2693
+ return c.body.some((n) => {
2694
+ if ("cond" in n)
2695
+ return false;
2696
+ if ("map" in n || "fanout" in n)
2697
+ return mapFanoutElemDeBoxEligible(n, tn, plan);
2698
+ return deBoxEligible(tn.get(n.id));
2699
+ });
2700
+ });
2701
+ // scripted wire adapter (TEST glue): wraps a scripted Value as a WireRow/WireList and classifies each
2702
+ // attribute's native type into a DynamoDB-flavored wire tag (S/N/BOOL/L/M/NULL). A real consumer
2703
+ // implements WireRow over its own AttributeValue map; this drives the generated de-box from the same
2704
+ // scripted vectors run_behavior uses — so actualWireType observed in a mismatch is the PRODUCER tag.
2705
+ const wireAdapter = anyDeBox
2706
+ ? `// ── scripted wire adapter (TEST glue) ──
2707
+ fn scripted_wire_tag(v: &Value) -> String {
2708
+ match v {
2709
+ Value::Null => "NULL",
2710
+ Value::Bool(_) => "BOOL",
2711
+ Value::Int(_) | Value::Float(_) => "N",
2712
+ Value::Str(_) => "S",
2713
+ Value::Arr(_) => "L",
2714
+ Value::Obj(_) => "M",
2715
+ }
2716
+ .to_string()
2717
+ }
2718
+
2719
+ fn scripted_wire_raw(v: &Value) -> String {
2720
+ match v {
2721
+ Value::Null => "null".to_string(),
2722
+ Value::Bool(b) => b.to_string(),
2723
+ Value::Int(n) => n.to_string(),
2724
+ Value::Float(f) => f.to_string(),
2725
+ Value::Str(s) => s.clone(),
2726
+ Value::Arr(_) | Value::Obj(_) => "[composite]".to_string(),
2727
+ }
2728
+ }
2729
+
2730
+ fn probe_string_val(v: Option<&Value>) -> Probe<String> {
2731
+ match v {
2732
+ None => Probe::Absent,
2733
+ Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2734
+ Some(Value::Str(s)) => Probe::Got(s.clone()),
2735
+ Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2736
+ }
2737
+ }
2738
+
2739
+ fn probe_number_val(v: Option<&Value>) -> NumProbe {
2740
+ match v {
2741
+ None => NumProbe::Absent,
2742
+ Some(Value::Null) => NumProbe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2743
+ Some(Value::Int(n)) => NumProbe::Got { raw: n.to_string(), actual_wire_type: "N".to_string() },
2744
+ Some(Value::Float(f)) => NumProbe::Got { raw: f.to_string(), actual_wire_type: "N".to_string() },
2745
+ Some(other) => NumProbe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2746
+ }
2747
+ }
2748
+
2749
+ fn probe_bool_val(v: Option<&Value>) -> Probe<bool> {
2750
+ match v {
2751
+ None => Probe::Absent,
2752
+ Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2753
+ Some(Value::Bool(b)) => Probe::Got(*b),
2754
+ Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2755
+ }
2756
+ }
2757
+
2758
+ fn probe_row_val(v: Option<&Value>) -> Probe<ScriptedWireRow> {
2759
+ match v {
2760
+ None => Probe::Absent,
2761
+ Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2762
+ Some(Value::Obj(pairs)) => Probe::Got(ScriptedWireRow { obj: pairs.clone() }),
2763
+ Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2764
+ }
2765
+ }
2766
+
2767
+ fn probe_list_val(v: Option<&Value>) -> Probe<ScriptedWireList> {
2768
+ match v {
2769
+ None => Probe::Absent,
2770
+ Some(Value::Null) => Probe::Null { actual_wire_type: "NULL".to_string(), raw_value: "null".to_string() },
2771
+ Some(Value::Arr(items)) => Probe::Got(ScriptedWireList { items: items.clone() }),
2772
+ Some(other) => Probe::Wrong { actual_wire_type: scripted_wire_tag(other), raw_value: scripted_wire_raw(other) },
2773
+ }
2774
+ }
2775
+
2776
+ pub struct ScriptedWireRow {
2777
+ obj: Vec<(String, Value)>,
2778
+ }
2779
+
2780
+ impl ScriptedWireRow {
2781
+ pub fn new(v: Value) -> Self {
2782
+ match v {
2783
+ Value::Obj(pairs) => ScriptedWireRow { obj: pairs },
2784
+ _ => ScriptedWireRow { obj: Vec::new() },
2785
+ }
2786
+ }
2787
+ fn get(&self, field: &str) -> Option<&Value> {
2788
+ self.obj.iter().find(|(k, _)| k.as_str() == field).map(|(_, v)| v)
2789
+ }
2790
+ }
2791
+
2792
+ pub struct ScriptedWireList {
2793
+ items: Vec<Value>,
2794
+ }
2795
+
2796
+ impl WireRow for ScriptedWireRow {
2797
+ type List = ScriptedWireList;
2798
+ fn keys(&self) -> Vec<String> {
2799
+ self.obj.iter().map(|(k, _)| k.clone()).collect()
2800
+ }
2801
+ fn probe_string(&self, field: &str) -> Probe<String> {
2802
+ probe_string_val(self.get(field))
2803
+ }
2804
+ fn probe_number(&self, field: &str) -> NumProbe {
2805
+ probe_number_val(self.get(field))
2806
+ }
2807
+ fn probe_bool(&self, field: &str) -> Probe<bool> {
2808
+ probe_bool_val(self.get(field))
2809
+ }
2810
+ fn probe_row(&self, field: &str) -> Probe<Self> {
2811
+ probe_row_val(self.get(field))
2812
+ }
2813
+ fn probe_list(&self, field: &str) -> Probe<Self::List> {
2814
+ probe_list_val(self.get(field))
2815
+ }
2816
+ }
2817
+
2818
+ impl WireList for ScriptedWireList {
2819
+ type Row = ScriptedWireRow;
2820
+ fn len(&self) -> usize {
2821
+ self.items.len()
2822
+ }
2823
+ fn elem_string(&self, i: usize) -> Probe<String> {
2824
+ probe_string_val(self.items.get(i))
2825
+ }
2826
+ fn elem_number(&self, i: usize) -> NumProbe {
2827
+ probe_number_val(self.items.get(i))
2828
+ }
2829
+ fn elem_bool(&self, i: usize) -> Probe<bool> {
2830
+ probe_bool_val(self.items.get(i))
2831
+ }
2832
+ fn elem_row(&self, i: usize) -> Probe<Self::Row> {
2833
+ probe_row_val(self.items.get(i))
2834
+ }
2835
+ fn elem_list(&self, i: usize) -> Probe<Self> {
2836
+ probe_list_val(self.items.get(i))
2837
+ }
2838
+ }`
2839
+ : "";
1989
2840
  // ── concrete scripted handler: implements every HandlerNR<comp> trait by decoding a scripted Value
1990
2841
  // into the concrete per-node row (the generic scripted-source queue meets the concrete seam). ──
1991
2842
  const decodeFns = [];
@@ -2009,22 +2860,39 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2009
2860
  // Option<K> (NOT a boxed Value) — computed per node so the impl signature matches the trait.
2010
2861
  const bt = boundRustType(n, typedNodes, plan);
2011
2862
  if ("fanout" in n) {
2012
- // v3: scripted fanout impl — drain one batched outcome, decode the aligned array into per-body
2013
- // elem rows (a null array element decodes to the zero elem row = dangling → dropped by the arm).
2014
2863
  const f = n.fanout;
2015
- const elemRowRef = fanoutItemsElemRef(n, ref, plan);
2864
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
2016
2865
  const elemT = rawElemStructName(c.name, n.id);
2017
2866
  const batchT = rawRowStructName(c.name, n.id);
2018
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2019
2867
  const portsT = `${portsStructName(c.name, n.id)}Batch`;
2020
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
2021
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)})?;
2868
+ if (elemDeBox) {
2869
+ // a record-element fanout returns the aligned element WIRES; a null aligned body is a DANGLING
2870
+ // id → None (the covered runner maps it to the zero elem row).
2871
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<Option<ScriptedWireRow>>, BehaviorError> {
2872
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
2873
+ if is_err {
2874
+ return Err(leaf_failure(err));
2875
+ }
2876
+ let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2877
+ let wires = arr
2878
+ .into_iter()
2879
+ .map(|ev| match ev {
2880
+ Value::Null => None,
2881
+ _ => Some(ScriptedWireRow::new(ev)),
2882
+ })
2883
+ .collect();
2884
+ Ok(wires)
2885
+ }`);
2886
+ continue;
2887
+ }
2888
+ const elemRowRef = fanoutItemsElemRef(n, ref, plan);
2889
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2890
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${batchT}, BehaviorError> {
2891
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)}).ok_or_else(|| unknown_component(${rustStrLit(f.component)}))?;
2022
2892
  if is_err {
2023
- return Some(${batchT} { is_error: true, err, ..Default::default() });
2893
+ return Err(leaf_failure(err));
2024
2894
  }
2025
2895
  let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2026
- // v3: a null aligned body is a DANGLING id — decode it to the zero elem row (whose dedupeKey
2027
- // field is empty), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
2028
2896
  let rows = arr
2029
2897
  .iter()
2030
2898
  .map(|ev| match ev {
@@ -2032,7 +2900,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2032
2900
  _ => ${elemDecode}(ev),
2033
2901
  })
2034
2902
  .collect();
2035
- Some(${batchT} { rows, ..Default::default() })
2903
+ Ok(${batchT} { rows })
2036
2904
  }`);
2037
2905
  continue;
2038
2906
  }
@@ -2040,70 +2908,172 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2040
2908
  const m = n.map;
2041
2909
  const batched = m.batched === true;
2042
2910
  const arrRef = ref;
2043
- const elemRowRef = mapElemRowRef(n, arrRef, plan);
2911
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
2044
2912
  const elemT = rawElemStructName(c.name, n.id);
2045
2913
  const batchT = rawRowStructName(c.name, n.id);
2046
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2047
2914
  const portsT = batched ? `${portsStructName(c.name, n.id)}Batch` : portsStructName(c.name, n.id);
2048
- const retT = batched ? batchT : elemT;
2915
+ if (elemDeBox) {
2916
+ // a record-element map returns the element WIRE(s); the covered runner de-boxes each strictly.
2917
+ if (batched) {
2918
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<Vec<ScriptedWireRow>, BehaviorError> {
2919
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2920
+ if is_err {
2921
+ return Err(leaf_failure(err));
2922
+ }
2923
+ let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2924
+ let wires = arr.into_iter().map(ScriptedWireRow::new).collect();
2925
+ Ok(wires)
2926
+ }`);
2927
+ }
2928
+ else {
2929
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<ScriptedWireRow, BehaviorError> {
2930
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2931
+ if is_err {
2932
+ return Err(leaf_failure(err));
2933
+ }
2934
+ Ok(ScriptedWireRow::new(ok.unwrap_or(Value::Null)))
2935
+ }`);
2936
+ }
2937
+ continue;
2938
+ }
2939
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
2940
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2049
2941
  if (batched) {
2050
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
2051
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
2942
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${batchT}, BehaviorError> {
2943
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2052
2944
  if is_err {
2053
- return Some(${batchT} { is_error: true, err, ..Default::default() });
2945
+ return Err(leaf_failure(err));
2054
2946
  }
2055
2947
  let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };
2056
2948
  let rows = arr.iter().map(${elemDecode}).collect();
2057
- Some(${batchT} { rows, ..Default::default() })
2949
+ Ok(${batchT} { rows })
2058
2950
  }`);
2059
2951
  }
2060
2952
  else {
2061
- methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
2062
- let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
2953
+ methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Result<${elemT}, BehaviorError> {
2954
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)}).ok_or_else(|| unknown_component(${rustStrLit(m.component)}))?;
2063
2955
  if is_err {
2064
- return Some(${elemT} { is_error: true, err, ..Default::default() });
2956
+ return Err(leaf_failure(err));
2065
2957
  }
2066
2958
  let v = ok.unwrap_or(Value::Null);
2067
- Some(${elemDecode}(&v))
2959
+ Ok(${elemDecode}(&v))
2068
2960
  }`);
2069
2961
  }
2070
- void retT;
2071
2962
  continue;
2072
2963
  }
2073
2964
  const node = n;
2074
2965
  const rowT = rawRowStructName(c.name, node.id);
2966
+ // a de-box-eligible node's scripted handler returns a ScriptedWireRow (the scripted Value wrapped as
2967
+ // a WireRow that classifies each attribute's native type into a DynamoDB-flavored wire tag); the
2968
+ // covered module's generated decode_wire_* then probes it. A real consumer returns its wire payload
2969
+ // for BC to de-box — the de-box no longer lives here.
2970
+ if (deBoxEligible(ref)) {
2971
+ // a record-containing top returns its wire (ScriptedWireRow / Vec / Option / BTreeMap) — the
2972
+ // covered runner de-boxes each record. #129: an arr node with an `items` port echoes the NATIVE
2973
+ // port array, serialized back to Vec<ScriptedWireRow> so the de-box round-trips.
2974
+ const top = recordTop(ref);
2975
+ const wireT = (() => {
2976
+ switch (top.kind) {
2977
+ case "named":
2978
+ return "ScriptedWireRow";
2979
+ case "arr":
2980
+ return "Vec<ScriptedWireRow>";
2981
+ case "opt":
2982
+ return "Option<ScriptedWireRow>";
2983
+ case "map":
2984
+ return "std::collections::BTreeMap<String, ScriptedWireRow>";
2985
+ }
2986
+ })();
2987
+ // echo-into-record (#141 fixtures): a named de-box node whose result WRAPS an input port — the
2988
+ // scripted leaf echoes port P into the record's single field, so the input-port value flows into
2989
+ // the de-boxed result and the runtime non-vacuity #139/#143/#144 assert survives the record wrap.
2990
+ let namedEcho = "";
2991
+ if (top.kind === "named") {
2992
+ const decl = findDecl(plan, top.innerName);
2993
+ const recordRust = renderTypeRef(ref);
2994
+ const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
2995
+ const fieldRust = field !== undefined ? renderTypeRef(field.type) : null;
2996
+ for (const pn of Object.keys(node.ports)) {
2997
+ let portRust = null;
2998
+ try {
2999
+ portRust = portFieldRustType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
3000
+ }
3001
+ catch {
3002
+ portRust = null;
3003
+ }
3004
+ if (portRust === recordRust) {
3005
+ // case 2: the port IS the whole record → echo it DIRECTLY (no field wrap), matching
3006
+ // run_behavior's plain echo:port of a whole-record port.
3007
+ const own = typeRefIsCopy(ref) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
3008
+ namedEcho += ` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireRow::new(${serializeTyped("tv", ref, plan)}));\n }\n`;
3009
+ }
3010
+ else if (fieldRust !== null && field !== undefined && portRust === fieldRust) {
3011
+ // case 1: a scalar port wraps into the record's single field ({v: ports.X}).
3012
+ const own = typeRefIsCopy(field.type) ? `ports.${portFieldName(pn)}` : `ports.${portFieldName(pn)}.clone()`;
3013
+ namedEcho += ` if echo == ${rustStrLit(pn)} {\n let tv = ${own};\n return Ok(ScriptedWireRow::new(Value::Obj(vec![(${rustStrLit(field.name)}.to_string(), ${serializeTyped("tv", field.type, plan)})])));\n }\n`;
3014
+ }
3015
+ }
3016
+ }
3017
+ const canEcho = (top.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) || namedEcho !== "";
3018
+ const portsParam = canEcho ? "ports" : "_ports";
3019
+ const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
3020
+ let body;
3021
+ if (top.kind === "named") {
3022
+ body = `${namedEcho} Ok(ScriptedWireRow::new(ok.unwrap_or(Value::Null)))`;
3023
+ }
3024
+ else if (top.kind === "arr") {
3025
+ const elemRef = ref.elem;
3026
+ const itemsF = portFieldName("items");
3027
+ const echoBranch = canEcho
3028
+ ? ` if echo == "items" {\n let wires: Vec<ScriptedWireRow> = ports.${itemsF}.iter().map(|e| { let tv = e.clone(); ScriptedWireRow::new(${serializeTyped("tv", elemRef, plan)}) }).collect();\n return Ok(wires);\n }\n`
3029
+ : "";
3030
+ body = `${echoBranch} let arr = match ok { Some(Value::Arr(a)) => a, _ => Vec::new() };\n Ok(arr.into_iter().map(ScriptedWireRow::new).collect())`;
3031
+ }
3032
+ else if (top.kind === "opt") {
3033
+ body = ` match ok {\n None | Some(Value::Null) => Ok(None),\n Some(v) => Ok(Some(ScriptedWireRow::new(v))),\n }`;
3034
+ }
3035
+ else {
3036
+ body = ` let obj = match ok { Some(Value::Obj(pairs)) => pairs, _ => Vec::new() };\n Ok(obj.into_iter().map(|(k, v)| (k, ScriptedWireRow::new(v))).collect())`;
3037
+ }
3038
+ methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<${wireT}, BehaviorError> {
3039
+ let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
3040
+ if is_err {
3041
+ return Err(leaf_failure(err));
3042
+ }
3043
+ ${body}
3044
+ }`);
3045
+ continue;
3046
+ }
2075
3047
  const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
2076
- // #129 (TEST glue): support the reference scriptedHandlers' echo forms (PROTOCOL.md §3.5) the
2077
- // adapter normalizes `{"echo":"port","port":"X"}` / `{"echo":"items"}` to the port NAME, and the
2078
- // scripted handler
2079
- // ECHOES a NATIVE port back as its result. This makes a port-plane pin non-vacuous: the node's
2080
- // observed result IS the native value that flowed into its ports struct, so a dropped / re-boxed /
2081
- // zero-defaulted port diverges from run_behavior instead of passing. A port is echoable when its
2082
- // native type IS the node's outType (that is exactly when the row's `val` can hold it); `ports` is
2083
- // bound only when at least one port qualifies.
2084
- const echoable = echoablePorts(c, node, ref, plan, typedNodes);
2085
- const portsParam = echoable.length > 0 ? "ports" : "_ports";
2086
- // a Copy value (i64/f64/bool/Option<scalar>) is read directly cloning it trips clippy clone_on_copy.
2087
- const own = (expr, r) => `${expr}${typeRefIsCopy(r) ? "" : ".clone()"}`;
2088
- // a NAMED outType row is FLATTENED (no `val`) — echo a named port by copying the struct's fields;
2089
- // a scalar/arr/opt outType row holds the port in its single `val` field.
2090
- const echoRow = (p) => ref.kind === "named"
2091
- ? `${rowT} { ${findDecl(plan, ref.name).fields.map((f) => `${rustFieldName(f.name)}: ${own(`${portsParam}.${portFieldName(p)}.${rustFieldName(f.name)}`, f.type)}`).join(", ")}, ..Default::default() }`
2092
- : `${rowT} { val: ${own(`${portsParam}.${portFieldName(p)}`, ref)}, ..Default::default() }`;
2093
- const echoBranch = echoable
2094
- .map((p) => `\n if echo == ${rustStrLit(p)} {\n return Some(${echoRow(p)});\n }`)
2095
- .join("");
2096
- const okBind = echoable.length > 0 ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
2097
- methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
2098
- let ${okBind} = self.next(${rustStrLit(node.component)})?;
3048
+ // #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
3049
+ // reference scriptedHandlers' `echo === "items"` the scripted handler ECHOES the NATIVE `f_items`
3050
+ // port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
3051
+ // observed result is exactly the native array that flowed into its ports struct, so a dropped or
3052
+ // boxed native array would diverge from run_behavior. `ports` is bound only on the echo path.
3053
+ const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
3054
+ const portsParam = canEcho ? "ports" : "_ports";
3055
+ const echoBranch = canEcho
3056
+ ? `\n if echo == "items" {\n return Ok(${rowT} { val: ${portsParam}.${portFieldName("items")}.clone() });\n }`
3057
+ : "";
3058
+ const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
3059
+ methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Result<${rowT}, BehaviorError> {
3060
+ let ${okBind} = self.next(${rustStrLit(node.component)}).ok_or_else(|| unknown_component(${rustStrLit(node.component)}))?;
2099
3061
  if is_err {
2100
- return Some(${rowT} { is_error: true, err, ..Default::default() });
3062
+ return Err(leaf_failure(err));
2101
3063
  }${echoBranch}
2102
3064
  let v = ok.unwrap_or(Value::Null);
2103
- Some(${decode}(&v))
3065
+ Ok(${decode}(&v))
2104
3066
  }`);
2105
3067
  }
2106
- traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${methods.join("\n")}\n}`);
3068
+ const cHasDeBox = c.body.some((n) => {
3069
+ if ("cond" in n)
3070
+ return false;
3071
+ if ("map" in n || "fanout" in n)
3072
+ return mapFanoutElemDeBoxEligible(n, typedNodes, plan);
3073
+ return deBoxEligible(typedNodes.get(n.id));
3074
+ });
3075
+ const wireAssoc = cHasDeBox ? ` type Wire = ScriptedWireRow;\n` : "";
3076
+ traitImpls.push(`impl ${handlerTraitName(c.name)} for ScriptedNativeRaw {\n${wireAssoc}${methods.join("\n")}\n}`);
2107
3077
  }
2108
3078
  const arms = native
2109
3079
  .map((c) => {
@@ -2152,7 +3122,7 @@ impl ScriptedNativeRaw {
2152
3122
  }
2153
3123
 
2154
3124
  // drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
2155
- // handlers); None when the component has no scripted queue (fail-closed). Returns (is_error, err,
3125
+ // handlers); None when the component has no scripted queue. Returns (is_error, err,
2156
3126
  // ok_value, echo). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
2157
3127
  // is Mutex-guarded. #129: \`echo\` carries the reference scriptedHandlers' \`echo\` directive (e.g.
2158
3128
  // "items") so a scripted node can ECHO a named input PORT back — used to make the node→leaf array
@@ -2162,13 +3132,11 @@ impl ScriptedNativeRaw {
2162
3132
  let mut queues = self.queues.lock().unwrap();
2163
3133
  let (queue, single) = queues.get_mut(component)?;
2164
3134
  let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
2165
- // PROTOCOL.md §3.5 echo forms, normalized to the port NAME: {"echo":"port","port":"X"} -> "X"
2166
- // ({"echo":"items"} is already that shape). The generated node method just compares this
2167
- // against its echoable port names.
2168
- let echo = match raw.get("echo").and_then(|e| e.as_str()) {
2169
- Some("port") => raw.get("port").and_then(|p| p.as_str()).unwrap_or("").to_string(),
2170
- Some(other) => other.to_string(),
2171
- None => String::new(),
3135
+ // normalize echo:port to the port NAME X so a de-box handler's echo comparison fires;
3136
+ // echo:items already names the port directly.
3137
+ let echo = {
3138
+ let e = raw.get("echo").and_then(|e| e.as_str()).unwrap_or("");
3139
+ if e == "port" { raw.get("port").and_then(|p| p.as_str()).unwrap_or("").to_string() } else { e.to_string() }
2172
3140
  };
2173
3141
  if let Some(okv) = raw.get("ok") {
2174
3142
  let v = behavior_contracts::decode_value(okv).expect("decode ok");
@@ -2196,9 +3164,27 @@ use behavior_contracts::Value;
2196
3164
  use super::*;
2197
3165
 
2198
3166
  // bridge the covered module's LOCAL BehaviorError (super::BehaviorError) to the bc-runtime failure type
2199
- // the conformance harness reads (same code + message) — the ONLY place the two meet (test glue).
3167
+ // the conformance harness reads (same code + message + structured detail) — the ONLY place the two meet
3168
+ // (test glue). The de-box's structured Error Value is carried over so the harness can assert it.
2200
3169
  fn to_rt_err(e: BehaviorError) -> behavior_contracts::BehaviorError {
2201
- behavior_contracts::BehaviorError::new(e.code, e.message)
3170
+ let mut rt = behavior_contracts::BehaviorError::new(e.code, e.message);
3171
+ if let Some(d) = e.detail {
3172
+ let d = *d;
3173
+ rt.detail = Some(Box::new(behavior_contracts::ErrorDetail {
3174
+ kind: d.kind.map(|k| match k {
3175
+ ErrorKind::TypeMismatch => behavior_contracts::ErrorKind::TypeMismatch,
3176
+ ErrorKind::MissingField => behavior_contracts::ErrorKind::MissingField,
3177
+ ErrorKind::Overflow => behavior_contracts::ErrorKind::Overflow,
3178
+ }),
3179
+ model: d.model,
3180
+ field: d.field,
3181
+ expected_type: d.expected_type,
3182
+ actual_wire_type: d.actual_wire_type,
3183
+ raw_value: d.raw_value,
3184
+ context: d.context,
3185
+ }));
3186
+ }
3187
+ rt
2202
3188
  }
2203
3189
  ${anyAsyncRunner
2204
3190
  ? `// bc#97 (TEST glue) — a minimal std-only block-on that drives an async runner Future to completion so
@@ -2238,6 +3224,9 @@ ${decodeFns.join("\n\n")}
2238
3224
  // input decoders (generic &[(String,Value)] -> concrete InNR_<comp>; TEST glue, one decode at the boundary).
2239
3225
  ${inDecoders}
2240
3226
 
3227
+ // scripted wire adapter (TEST glue) — wraps a scripted Value as a WireRow the generated de-box probes.
3228
+ ${wireAdapter}
3229
+
2241
3230
  ${adapter}
2242
3231
 
2243
3232
  // scripted handler trait impls (one per covered component — the concrete node_* seam, test glue).
@@ -2260,10 +3249,8 @@ ${arms}
2260
3249
  }
2261
3250
  /** emitInDecoder — a TEST-glue decoder `decode_InNR_<comp>(input) -> Result<InNR_<comp>, BehaviorError>`.
2262
3251
  * Reads each declared input port from the generic &[(String,Value)] into the concrete field (scalar →
2263
- * typed read; opt → Some/None; Value-typed → the raw Value clone). A key that is absent, or present as
2264
- * `Value::Null`, leaves the field's Default for an OPTIONAL port that is `None` (absent), which is the
2265
- * same value `refCore` observes for the `null` binding run_behavior is given. Lives ONLY in the observe
2266
- * companion. */
3252
+ * typed read; Value-typed → the raw Value clone). A missing key leaves Default (every covered input is
3253
+ * required + supplied by the corpus). Lives ONLY in the observe companion. */
2267
3254
  function emitInDecoder(comp, plan) {
2268
3255
  const name = inStructName(comp.name);
2269
3256
  const ports = Object.keys(comp.inputPorts ?? {});
@@ -2285,15 +3272,7 @@ function emitInDecoder(comp, plan) {
2285
3272
  const et = schema.elemType;
2286
3273
  const head = i === 0 ? " if" : " } else if";
2287
3274
  lines.push(`${head} k == ${rustStrLit(p)} {`);
2288
- if (inputPortIsOptional(schema)) {
2289
- // An OPTIONAL port decodes into its native Option<T>. materializeExpr's opt de-box IS the wire
2290
- // rule: `Value::Null` → None (absent), any other value → Some(<inner>). A key that never appears
2291
- // keeps the struct Default (None) — the same absent value. Checked FIRST: optionality wraps every
2292
- // inner type (an optional array is Option<Vec<T>>, not Vec<T>).
2293
- const optRef = inputPortTypeRef(schema, plan);
2294
- lines.push(` in_.${field} = ${materializeExpr("v", optRef, plan)};`);
2295
- }
2296
- else if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
3275
+ if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
2297
3276
  // #108: decode the boxed Value::Arr into the native Vec<ElemT> (test glue — materialize each elem).
2298
3277
  const elemRef = deriveTypeRef(et, plan);
2299
3278
  const elemTy = renderTypeRef(elemRef);
@@ -2313,6 +3292,11 @@ function emitInDecoder(comp, plan) {
2313
3292
  const mapRef = { kind: "map", value: deriveTypeRef(et, plan) };
2314
3293
  lines.push(` in_.${field} = ${materializeExpr("v", mapRef, plan)};`);
2315
3294
  }
3295
+ else if (inputPortIsOptional(schema)) {
3296
+ // #139: an OPTIONAL scalar port decodes into Option<T> via the inputPortTypeRef SSoT (a present key
3297
+ // materializes Some(v); an absent key never enters this block, leaving the Default None).
3298
+ lines.push(` in_.${field} = ${materializeExpr("v", inputPortTypeRef(schema, plan), plan)};`);
3299
+ }
2316
3300
  else if (scalar === undefined) {
2317
3301
  lines.push(` in_.${field} = v.clone();`);
2318
3302
  }
@@ -2331,14 +3315,6 @@ function emitInDecoder(comp, plan) {
2331
3315
  lines.push(`}`);
2332
3316
  return lines.join("\n");
2333
3317
  }
2334
- /** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
2335
- * declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row (a scalar/arr/
2336
- * opt row via its `val`; a NAMED row via its flattened fields copied from the struct port). A port with a
2337
- * different type is not echoable (the row could not hold it). */
2338
- function echoablePorts(comp, node, ref, plan, typedNodes) {
2339
- const want = renderTypeRef(ref);
2340
- return Object.keys(node.ports).filter((p) => portFieldRustType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
2341
- }
2342
3318
  /** emitDecodeRow — emit (once, deduped) a decoder `decode_row_<comp>_<node><suffix>(v: &Value) -> <rowT>`
2343
3319
  * that materializes the scripted ok Value into the concrete row struct. Named: reuse the Value->struct
2344
3320
  * marshaller + move fields; scalar/arr/opt: materialize into .val. TEST glue only. */
@@ -2353,12 +3329,12 @@ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix)
2353
3329
  const decl = findDecl(plan, ref.name);
2354
3330
  lines.push(` let t = marshal_${ref.name}(v).expect("scripted row decode");`);
2355
3331
  const inits = decl.fields.map((f) => `${rustFieldName(f.name)}: t.${rustFieldName(f.name)}`).join(", ");
2356
- lines.push(` ${rowT} { ${inits}, ..Default::default() }`);
3332
+ lines.push(` ${rowT} { ${inits} }`);
2357
3333
  }
2358
3334
  else {
2359
3335
  const mat = materializeExpr("v", ref, plan);
2360
3336
  lines.push(` let val = (|| -> Result<${renderTypeRef(ref)}, BehaviorError> { Ok(${mat}) })().expect("scripted row decode");`);
2361
- lines.push(` ${rowT} { val, ..Default::default() }`);
3337
+ lines.push(` ${rowT} { val }`);
2362
3338
  }
2363
3339
  lines.push(`}`);
2364
3340
  out.push(lines.join("\n"));