behavior-contracts 0.8.4 → 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.
@@ -15,14 +15,34 @@ function goErr(code, msgExpr) {
15
15
  return `&BehaviorError{Code: ${JSON.stringify(code)}, Message: ${msgExpr}}`;
16
16
  }
17
17
  /** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
18
- * fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error`. */
18
+ * fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error` and carries the leaf's
19
+ * structured Error Value (scp-error.md "The Error Value"). */
19
20
  function emitBehaviorErrorType() {
20
- return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
21
+ return `// ErrorDetail — the structured, recoverable payload a failure carries (scp-error.md "The Error
22
+ // Value"). The LEAF produces it at the wire boundary (the only party holding both the declared type
23
+ // and the raw wire datum); the runner transports it verbatim. Concrete fields — no boxed Value, no
24
+ // serialized blob in a string: ExpectedType is Portable Type Notation, a rendering of a STATICALLY
25
+ // DECLARED type, so nothing walks a type at runtime. Kind is the closed set (typeMismatch /
26
+ // missingField / overflow), empty when the failure is not about a datum.
27
+ type ErrorDetail struct {
28
+ Kind string
29
+ Model string
30
+ Field string
31
+ ExpectedType string
32
+ ActualWireType string
33
+ RawValue string
34
+ Context map[string]string
35
+ }
36
+
37
+ // BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
21
38
  // covered runner returns *BehaviorError (which satisfies error) instead of a bc-runtime failure, so
22
39
  // the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim (byte-equal).
40
+ // Detail carries the leaf's structured Error Value across the seam so a caller can log / skip /
41
+ // repair / migrate rather than parse a message (nil when the failure is not about a datum).
23
42
  type BehaviorError struct {
24
43
  Code string
25
44
  Message string
45
+ Detail *ErrorDetail
26
46
  }
27
47
 
28
48
  func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
@@ -30,7 +50,48 @@ func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
30
50
  // FailureCode exposes the stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type:
31
51
  // a caller that wants the code type-asserts the small local interface { FailureCode() string } rather
32
52
  // than a bc failure type — keeping this module runtime-free while a harness can still read the code.
33
- func (e *BehaviorError) FailureCode() string { return e.Code }`;
53
+ func (e *BehaviorError) FailureCode() string { return e.Code }
54
+
55
+ // unknownComponent — the fail-closed failure for a catalog name with no handler (byte-equal to
56
+ // run_behavior). In the STATIC codegen model a missing impl is normally a compile error; this covers
57
+ // the scripted test glue's dynamic queue miss.
58
+ func unknownComponent(component string) *BehaviorError {
59
+ return &BehaviorError{Code: "UNKNOWN_COMPONENT", Message: "component '" + component + "' has no handler (fail-closed)"}
60
+ }
61
+
62
+ // leafFailure — a failure the LEAF reports (test glue): the runner assigns the node's failure code
63
+ // from its Error Policy Kind, so what a leaf carries is its message and its structured Error Value.
64
+ func leafFailure(message string) *BehaviorError {
65
+ return &BehaviorError{Code: "LEAF_FAILURE", Message: message}
66
+ }
67
+
68
+ // opFailed wraps a leaf failure as the node's Failure under its Error Policy Kind, carrying the
69
+ // leaf's structured detail through unchanged (the runner transports the Error Value, it does not
70
+ // synthesize or re-interpret it).
71
+ func opFailed(node string, policy string, e error) *BehaviorError {
72
+ if be, ok := e.(*BehaviorError); ok {
73
+ return &BehaviorError{Code: "OP_FAILED", Message: "operation '" + node + "' failed under '" + policy + "' policy: " + be.Message, Detail: be.Detail}
74
+ }
75
+ return &BehaviorError{Code: "OP_FAILED", Message: "operation '" + node + "' failed under '" + policy + "' policy: " + e.Error()}
76
+ }
77
+
78
+ // deTypeMismatch / deMissingField / deOverflow — the de-box mismatch failures the generated decode
79
+ // produces (scp-error.md "The Error Value"). Codes are byte-equal to the runtime outType check
80
+ // (TYPE_MISMATCH / MISSING_PROP) so the covered read stays equivalent to run_behavior; detail.kind
81
+ // distinguishes typeMismatch / missingField / overflow. ErrorDetail is built by a POSITIONAL literal so
82
+ // the offending value is carried without naming the field (the covered module stays whole-file free of
83
+ // the boxed result-type token; its only permitted occurrence is the ErrorDetail field declaration).
84
+ func deTypeMismatch(model, field, expected, actualWire, raw string) *BehaviorError {
85
+ return &BehaviorError{Code: "TYPE_MISMATCH", Message: "node '" + model + "': " + field + ": expected " + expected + ", got " + actualWire, Detail: &ErrorDetail{"typeMismatch", model, field, expected, actualWire, raw, nil}}
86
+ }
87
+
88
+ func deMissingField(model, field, expected string) *BehaviorError {
89
+ return &BehaviorError{Code: "MISSING_PROP", Message: "node '" + model + "': " + field + ": required field is absent", Detail: &ErrorDetail{"missingField", model, field, expected, "", "", nil}}
90
+ }
91
+
92
+ func deOverflow(model, field, expected, actualWire, raw string) *BehaviorError {
93
+ return &BehaviorError{Code: "TYPE_MISMATCH", Message: "node '" + model + "': " + field + ": " + raw + " is outside the range of " + expected, Detail: &ErrorDetail{"overflow", model, field, expected, actualWire, raw, nil}}
94
+ }`;
34
95
  }
35
96
  const { sanitize, emitExpr } = goStraightlineInternals;
36
97
  const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emitSerializers, emitTypedValue, emitMustHelpers, } = goTypedInternals;
@@ -39,6 +100,510 @@ const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emi
39
100
  function typedLocal(nodeId) {
40
101
  return `t_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
41
102
  }
103
+ function recordTop(ref) {
104
+ if (ref.kind === "named")
105
+ return { kind: "named", innerName: ref.name };
106
+ if (ref.kind === "arr" && ref.elem.kind === "named")
107
+ return { kind: "arr", innerName: ref.elem.name };
108
+ if (ref.kind === "opt" && ref.inner.kind === "named")
109
+ return { kind: "opt", innerName: ref.inner.name };
110
+ if (ref.kind === "map" && ref.value.kind === "named")
111
+ return { kind: "map", innerName: ref.value.name };
112
+ return null;
113
+ }
114
+ /** deBoxEligible — a componentRef node whose result is a record-containing top (named / arr(named) /
115
+ * opt(named) / map(named)). Its handler returns the wire; the generated decode probes each record. */
116
+ function deBoxEligible(ref) {
117
+ return recordTop(ref) !== null;
118
+ }
119
+ /** goWireTypeForTop — the Go handler return type for a record-containing top's wire. */
120
+ function goWireTypeForTop(top) {
121
+ switch (top.kind) {
122
+ case "named":
123
+ return "WireRow";
124
+ case "arr":
125
+ return "[]WireRow";
126
+ case "opt":
127
+ return "WireRow"; // a nil WireRow = None (opt admits absent)
128
+ case "map":
129
+ return "map[string]WireRow";
130
+ }
131
+ }
132
+ /** notationOfRef — the Portable Type Notation of a TypeRef (scp-error.md), resolving a named ref to its
133
+ * obj notation via the plan. Baked as the `expectedType` literal: a rendering of the STATICALLY declared
134
+ * type, so nothing walks a type at runtime. */
135
+ function notationOfRef(ref, plan) {
136
+ switch (ref.kind) {
137
+ case "scalar":
138
+ return ref.scalar;
139
+ case "opt":
140
+ return `opt(${notationOfRef(ref.inner, plan)})`;
141
+ case "arr":
142
+ return `arr(${notationOfRef(ref.elem, plan)})`;
143
+ case "map":
144
+ return `map(${notationOfRef(ref.value, plan)})`;
145
+ case "named": {
146
+ const decl = findDecl(plan, ref.name);
147
+ return `obj{${decl.fields.map((f) => `${f.name}:${notationOfRef(f.type, plan)}`).join(",")}}`;
148
+ }
149
+ }
150
+ }
151
+ /** emitProbeWireTypes — the once-per-module probe structs + WireRow/WireList seam interfaces. A
152
+ * NumberProbe's Got is the raw numeric text (the de-box parses + range-checks it, so overflow is BC's
153
+ * to detect); every probe carries ActualWireType even on a match so overflow can report the numeric
154
+ * wire tag. Raw is the offending value stringified (NOT named RawValue — that token is reserved for the
155
+ * ErrorDetail field decl). Concrete structs / named interfaces — no boxed Value on the seam. */
156
+ function emitProbeWireTypes() {
157
+ return `// Probe kinds — the outcome of classifying one wire attribute against a declared field type.
158
+ const (
159
+ probeGot uint8 = 0 // present; the wire variant matches the declared type
160
+ probeWrong uint8 = 1 // present but a different wire variant (carries ActualWireType + Raw)
161
+ probeAbsent uint8 = 2 // no attribute for this field
162
+ probeNull uint8 = 3 // present as the producer's null variant (opt → None; required → mismatch)
163
+ )
164
+
165
+ // StringProbe / NumberProbe / BoolProbe — a scalar probe result. Got holds the matched value (a
166
+ // NumberProbe's Got is the raw numeric text; the de-box parses + range-checks it). ActualWireType is the
167
+ // producer's own wire tag (a free string, e.g. a DynamoDB "S"/"N"/"BOOL"); Raw is the offending value
168
+ // stringified. Concrete structs — no boxed Value crosses the seam.
169
+ type StringProbe struct {
170
+ Kind uint8
171
+ Got string
172
+ ActualWireType string
173
+ Raw string
174
+ }
175
+
176
+ type NumberProbe struct {
177
+ Kind uint8
178
+ Got string
179
+ ActualWireType string
180
+ Raw string
181
+ }
182
+
183
+ type BoolProbe struct {
184
+ Kind uint8
185
+ Got bool
186
+ ActualWireType string
187
+ Raw string
188
+ }
189
+
190
+ // RowProbe / ListProbe — a composite probe result: a nested WireRow (a DynamoDB "M") or WireList (an "L").
191
+ type RowProbe struct {
192
+ Kind uint8
193
+ Got WireRow
194
+ ActualWireType string
195
+ Raw string
196
+ }
197
+
198
+ type ListProbe struct {
199
+ Kind uint8
200
+ Got WireList
201
+ ActualWireType string
202
+ Raw string
203
+ }
204
+
205
+ // WireRow — the consumer's wire item, probed per declared field by the generated de-box. The consumer
206
+ // implements it over its wire payload (a DynamoDB AttributeValue map) and classifies each attribute's
207
+ // variant; the generated de-box owns strictness. No boxed Value, no dynamic accessor.
208
+ type WireRow interface {
209
+ // Keys reports the attribute keys present (the entries of a wire map / DynamoDB "M"); the de-box
210
+ // iterates them to decode a declared map(V) field.
211
+ Keys() []string
212
+ ProbeString(field string) StringProbe
213
+ ProbeNumber(field string) NumberProbe
214
+ ProbeBool(field string) BoolProbe
215
+ ProbeRow(field string) RowProbe
216
+ ProbeList(field string) ListProbe
217
+ }
218
+
219
+ // WireList — a wire array (a DynamoDB "L"), probed per element by the generated de-box.
220
+ type WireList interface {
221
+ Len() int
222
+ ElemString(i int) StringProbe
223
+ ElemNumber(i int) NumberProbe
224
+ ElemBool(i int) BoolProbe
225
+ ElemRow(i int) RowProbe
226
+ ElemList(i int) ListProbe
227
+ }`;
228
+ }
229
+ /** goDecodeFnName — the decode function name for a named type (deduped across the module). */
230
+ function goDecodeNamedName(name) {
231
+ return `decodeWire_${name.replace(/[^A-Za-z0-9_]/g, "_")}`;
232
+ }
233
+ /**
234
+ * emitDecodeNamed — emit (once, deduped) `decodeWire_<Name>(w WireRow) (<GoType>, error)` for a named
235
+ * type: probe each declared field from the type SSoT, produce the structured error on a mismatch
236
+ * (deTypeMismatch / deMissingField / deOverflow), and recurse into nested named / arr fields. `model`
237
+ * is the declaring type name (scp-error.md `model`). Nested named types are emitted transitively.
238
+ */
239
+ function emitDecodeNamed(name, plan, emitted, out) {
240
+ const fn = goDecodeNamedName(name);
241
+ if (emitted.has(fn))
242
+ return fn;
243
+ emitted.add(fn);
244
+ const goType = name; // a named TypeRef renders to its own Go type name.
245
+ const decl = findDecl(plan, name);
246
+ const lines = [];
247
+ lines.push(`// ${fn} — the strict de-box for '${name}': probe each declared field, fail closed on a`);
248
+ lines.push(`// wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
249
+ lines.push(`func ${fn}(w WireRow) (${goType}, error) {`);
250
+ lines.push(`\tvar out ${goType}`);
251
+ const ctr = { n: 0 };
252
+ for (const f of decl.fields) {
253
+ lines.push(emitDecodeValue(`out.${goFieldName(f.name)}`, f.type, "w", JSON.stringify(f.name), false, name, f.name, `${goType}{}`, plan, emitted, out, 1, ctr));
254
+ }
255
+ lines.push(`\treturn out, nil`);
256
+ lines.push(`}`);
257
+ out.push(lines.join("\n"));
258
+ return fn;
259
+ }
260
+ /**
261
+ * emitDecodeValue — the SINGLE recursive decode over the full TypeRef grammar (scalar / opt(scalar) /
262
+ * named / arr / map). It decodes one value of `ref`, obtained by probing `wireVar` at `keyExpr`, and
263
+ * assigns it into the lvalue `target`. Every nested position (an arr element, a map value, an obj field)
264
+ * recurses through THIS function, so arbitrary nesting (map-of-map, arr-of-map, …) works by construction
265
+ * — there is no depth-specific decoder to leave a gap. A mismatch produces the structured Error Value
266
+ * (deTypeMismatch / deMissingField / deOverflow) and returns `retZero`; no panic, no boxed value.
267
+ *
268
+ * - `keyExpr` the Go probe key: a quoted literal `"title"` (obj field), a map-key var, or a list index.
269
+ * - `isElem` probe a WireList element (Elem*, no absent — an index always exists) vs a WireRow field
270
+ * / map key (Probe*, absent → missingField).
271
+ * - `fieldRaw` the raw error `field` label (scp-error.md). `ctr` gives every temp a unique suffix.
272
+ */
273
+ function emitDecodeValue(target, ref, wireVar, keyExpr, isElem, model, fieldRaw, retZero, plan, emitted, out, indent, ctr) {
274
+ const t = "\t".repeat(indent);
275
+ const ml = JSON.stringify(model);
276
+ const fl = JSON.stringify(fieldRaw);
277
+ const notation = notationOfRef(ref, plan);
278
+ const nl = JSON.stringify(notation);
279
+ const id = `_${ctr.n++}`;
280
+ const lines = [];
281
+ const rowProbe = `${wireVar}.${isElem ? "ElemRow" : "ProbeRow"}(${keyExpr})`;
282
+ const listProbe = `${wireVar}.${isElem ? "ElemList" : "ProbeList"}(${keyExpr})`;
283
+ // an absent attribute is a missing REQUIRED field (a WireList index always exists → no absent check).
284
+ const absent = (pv) => isElem ? "" : `${t}\tif ${pv}.Kind == probeAbsent {\n${t}\t\treturn ${retZero}, deMissingField(${ml}, ${fl}, ${nl})\n${t}\t}\n`;
285
+ const mismatch = (pv, ind) => `${ind}return ${retZero}, deTypeMismatch(${ml}, ${fl}, ${nl}, ${pv}.ActualWireType, ${pv}.Raw)`;
286
+ // opt(scalar): Absent / Null → leave the pointer nil (valid, no error); Wrong → mismatch; Got → &value.
287
+ if (ref.kind === "opt" && ref.inner.kind === "scalar" && ref.inner.scalar !== "null") {
288
+ const pv = `p${id}`;
289
+ lines.push(`${t}{`);
290
+ lines.push(`${t}\t${pv} := ${scalarProbeCall(wireVar, keyExpr, ref.inner.scalar, isElem).call}`);
291
+ lines.push(`${t}\tif ${pv}.Kind == probeGot {`);
292
+ lines.push(scalarAssignInto(target, pv, ref.inner.scalar, model, fieldRaw, notation, `${t}\t\t`, retZero, true));
293
+ lines.push(`${t}\t} else if ${pv}.Kind == probeWrong {`);
294
+ lines.push(mismatch(pv, `${t}\t\t`));
295
+ lines.push(`${t}\t}`);
296
+ lines.push(`${t}}`);
297
+ return lines.join("\n");
298
+ }
299
+ if (ref.kind === "scalar" && ref.scalar !== "null") {
300
+ const pv = `p${id}`;
301
+ lines.push(`${t}{`);
302
+ lines.push(`${t}\t${pv} := ${scalarProbeCall(wireVar, keyExpr, ref.scalar, isElem).call}`);
303
+ if (!isElem)
304
+ lines.push(absent(pv).replace(/\n$/, ""));
305
+ lines.push(`${t}\tif ${pv}.Kind != probeGot {`); // Wrong or Null on a required value → mismatch
306
+ lines.push(mismatch(pv, `${t}\t\t`));
307
+ lines.push(`${t}\t}`);
308
+ lines.push(scalarAssignInto(target, pv, ref.scalar, model, fieldRaw, notation, `${t}\t`, retZero, false));
309
+ lines.push(`${t}}`);
310
+ return lines.join("\n");
311
+ }
312
+ if (ref.kind === "named") {
313
+ const dec = emitDecodeNamed(ref.name, plan, emitted, out);
314
+ const rp = `r${id}`;
315
+ lines.push(`${t}{`);
316
+ lines.push(`${t}\t${rp} := ${rowProbe}`);
317
+ if (!isElem)
318
+ lines.push(absent(rp).replace(/\n$/, ""));
319
+ lines.push(`${t}\tif ${rp}.Kind != probeGot {`);
320
+ lines.push(mismatch(rp, `${t}\t\t`));
321
+ lines.push(`${t}\t}`);
322
+ lines.push(`${t}\tv${id}, e${id} := ${dec}(${rp}.Got)`);
323
+ lines.push(`${t}\tif e${id} != nil {`);
324
+ lines.push(`${t}\t\treturn ${retZero}, e${id}`);
325
+ lines.push(`${t}\t}`);
326
+ lines.push(`${t}\t${target} = v${id}`);
327
+ lines.push(`${t}}`);
328
+ return lines.join("\n");
329
+ }
330
+ if (ref.kind === "arr") {
331
+ const lp = `l${id}`;
332
+ const elemGo = renderTypeRef(ref.elem);
333
+ lines.push(`${t}{`);
334
+ lines.push(`${t}\t${lp} := ${listProbe}`);
335
+ if (!isElem)
336
+ lines.push(absent(lp).replace(/\n$/, ""));
337
+ lines.push(`${t}\tif ${lp}.Kind != probeGot {`);
338
+ lines.push(mismatch(lp, `${t}\t\t`));
339
+ lines.push(`${t}\t}`);
340
+ lines.push(`${t}\t${target} = make([]${elemGo}, 0, ${lp}.Got.Len())`);
341
+ lines.push(`${t}\tfor i${id} := 0; i${id} < ${lp}.Got.Len(); i${id}++ {`);
342
+ lines.push(`${t}\t\tvar el${id} ${elemGo}`);
343
+ lines.push(emitDecodeValue(`el${id}`, ref.elem, `${lp}.Got`, `i${id}`, true, model, fieldRaw, retZero, plan, emitted, out, indent + 2, ctr));
344
+ lines.push(`${t}\t\t${target} = append(${target}, el${id})`);
345
+ lines.push(`${t}\t}`);
346
+ lines.push(`${t}}`);
347
+ return lines.join("\n");
348
+ }
349
+ if (ref.kind === "map") {
350
+ // a declared map(V): the wire value is a producer map (a DynamoDB "M"); enumerate its keys and decode
351
+ // each value as V through THIS same recursion (so map-of-map / map-of-arr nest by construction).
352
+ const mp = `m${id}`;
353
+ const valGo = renderTypeRef(ref.value);
354
+ lines.push(`${t}{`);
355
+ lines.push(`${t}\t${mp} := ${rowProbe}`);
356
+ if (!isElem)
357
+ lines.push(absent(mp).replace(/\n$/, ""));
358
+ lines.push(`${t}\tif ${mp}.Kind != probeGot {`);
359
+ lines.push(mismatch(mp, `${t}\t\t`));
360
+ lines.push(`${t}\t}`);
361
+ lines.push(`${t}\t${target} = make(map[string]${valGo})`);
362
+ lines.push(`${t}\tfor _, k${id} := range ${mp}.Got.Keys() {`);
363
+ lines.push(`${t}\t\tvar mv${id} ${valGo}`);
364
+ lines.push(emitDecodeValue(`mv${id}`, ref.value, `${mp}.Got`, `k${id}`, false, model, fieldRaw, retZero, plan, emitted, out, indent + 2, ctr));
365
+ lines.push(`${t}\t\t${target}[k${id}] = mv${id}`);
366
+ lines.push(`${t}\t}`);
367
+ lines.push(`${t}}`);
368
+ return lines.join("\n");
369
+ }
370
+ // fail closed at generation for a shape the grammar does not cover (e.g. opt of a non-scalar).
371
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go 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).`);
372
+ }
373
+ /** scalarProbeCall — the probe method call for a scalar of the given kind (string→ProbeString/ElemString,
374
+ * int|float→ProbeNumber/ElemNumber, bool→ProbeBool/ElemBool). `keyOrIndex` is a quoted key (row) or an
375
+ * index var (list). */
376
+ function scalarProbeCall(wireVar, keyOrIndex, scalar, isElem) {
377
+ const m = scalar === "string" ? "String" : scalar === "bool" ? "Bool" : "Number";
378
+ const method = (isElem ? "Elem" : "Probe") + m;
379
+ return { call: `${wireVar}.${method}(${keyOrIndex})` };
380
+ }
381
+ /** scalarAssignInto — assign a probeGot scalar into an lvalue `target` (opt → pointer). Number is parsed
382
+ * + range-checked here (BC owns overflow). No panic on parse failure — a structured overflow error. The
383
+ * `n/nErr/f/fErr/v` temps are safe because every scalar decode is wrapped in its own `{ }` block. */
384
+ function scalarAssignInto(fieldExpr, pv, scalar, model, key, notation, t, retZero, asPtr) {
385
+ const ml = JSON.stringify(model);
386
+ const kl = JSON.stringify(key);
387
+ const nl = JSON.stringify(notation);
388
+ if (scalar === "string") {
389
+ return asPtr ? `${t}v := ${pv}.Got\n${t}${fieldExpr} = &v` : `${t}${fieldExpr} = ${pv}.Got`;
390
+ }
391
+ if (scalar === "bool") {
392
+ return asPtr ? `${t}v := ${pv}.Got\n${t}${fieldExpr} = &v` : `${t}${fieldExpr} = ${pv}.Got`;
393
+ }
394
+ if (scalar === "int") {
395
+ const l = [];
396
+ l.push(`${t}n, nErr := strconv.ParseInt(${pv}.Got, 10, 64)`);
397
+ l.push(`${t}if nErr != nil {`);
398
+ l.push(`${t}\treturn ${retZero}, deOverflow(${ml}, ${kl}, ${nl}, ${pv}.ActualWireType, ${pv}.Got)`);
399
+ l.push(`${t}}`);
400
+ l.push(asPtr ? `${t}${fieldExpr} = &n` : `${t}${fieldExpr} = n`);
401
+ return l.join("\n");
402
+ }
403
+ // float — int widens to float; a non-numeric raw is a range/parse failure (overflow kind).
404
+ const l = [];
405
+ l.push(`${t}f, fErr := strconv.ParseFloat(${pv}.Got, 64)`);
406
+ l.push(`${t}if fErr != nil {`);
407
+ l.push(`${t}\treturn ${retZero}, deOverflow(${ml}, ${kl}, ${nl}, ${pv}.ActualWireType, ${pv}.Got)`);
408
+ l.push(`${t}}`);
409
+ l.push(asPtr ? `${t}${fieldExpr} = &f` : `${t}${fieldExpr} = f`);
410
+ return l.join("\n");
411
+ }
412
+ /** goDecodeElemName — the element de-box function name for a map/fanout node (per node). */
413
+ function goDecodeElemName(compName, nodeId) {
414
+ return `decodeWireElem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
415
+ }
416
+ /** mapFanoutElemRef — the element ROW TypeRef a map/fanout node decodes (the per-element record). */
417
+ function mapFanoutElemRef(node, typedNodes, plan) {
418
+ const ref = typedNodes.get(node.id);
419
+ if ("fanout" in node)
420
+ return fanoutItemsElemRef(node, ref, plan);
421
+ return mapElemRowRef(node, ref, plan);
422
+ }
423
+ /** mapFanoutElemDeBoxEligible — a map/fanout node whose element ROW is a named record de-boxes each
424
+ * element through the generated decodeWireElem_* (closing the scan-row silent-default hole). */
425
+ function mapFanoutElemDeBoxEligible(node, typedNodes, plan) {
426
+ return mapFanoutElemRef(node, typedNodes, plan).kind === "named";
427
+ }
428
+ /**
429
+ * emitDecodeElemRow — emit (once) `decodeWireElem_<comp>_<node>(w WireRow) (RawElem_<comp>_<node>, error)`
430
+ * for a map/fanout node whose element ROW is a named record: probe each declared field via the SAME
431
+ * recursion as record reads (emitDecodeValue) and build the concrete RawElem struct, failing closed with
432
+ * the structured Error Value on a mismatch. This is the componentRef seam extended to scan/fan-out rows.
433
+ */
434
+ function emitDecodeElemRow(comp, node, elemRowRef, plan, emitted, out) {
435
+ const nodeId = node.id;
436
+ const fn = goDecodeElemName(comp.name, nodeId);
437
+ if (emitted.has(fn))
438
+ return fn;
439
+ emitted.add(fn);
440
+ const retType = rawElemStructName(comp.name, nodeId);
441
+ const decl = findDecl(plan, elemRowRef.name);
442
+ const lines = [];
443
+ lines.push(`// ${fn} — the strict de-box for a '${nodeId}' element row: probe each declared field, fail`);
444
+ lines.push(`// closed on a wire mismatch with the structured Error Value (byte-equal codes to run_behavior).`);
445
+ lines.push(`func ${fn}(w WireRow) (${retType}, error) {`);
446
+ lines.push(`\tvar out ${retType}`);
447
+ const ctr = { n: 0 };
448
+ for (const f of decl.fields) {
449
+ lines.push(emitDecodeValue(`out.${goFieldName(f.name)}`, f.type, "w", JSON.stringify(f.name), false, elemRowRef.name, f.name, `${retType}{}`, plan, emitted, out, 1, ctr));
450
+ }
451
+ lines.push(`\treturn out, nil`);
452
+ lines.push(`}`);
453
+ out.push(lines.join("\n"));
454
+ return fn;
455
+ }
456
+ /** emitNodeDecodeFns — emit the decode functions for every de-box-eligible node in a component: a
457
+ * componentRef record read (decodeWire_<Name>) OR a map/fanout element row (decodeWireElem_<comp>_<node>).
458
+ * Deduped; nested named fields recurse through the shared decodeWire_<Name>. */
459
+ /** goDecodeResultName — the top-result decode function name for a de-box componentRef node. */
460
+ function goDecodeResultName(compName, nodeId) {
461
+ return `decodeWireResult_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
462
+ }
463
+ /**
464
+ * emitDecodeWireResult — emit (once) `decodeWireResult_<comp>_<node>(wire <WireType>) (<ResultType>,
465
+ * error)` for a componentRef node whose result is a record-containing top: it decodes each record via
466
+ * decodeWire_<InnerName> and assembles the top (a record / []record / *record / map[string]record),
467
+ * failing closed with the structured Error Value on a mismatch. This is the SAME decode recursion at the
468
+ * node-result position — one path covers named / arr(named) / opt(named) / map(named).
469
+ */
470
+ function emitDecodeWireResult(comp, node, ref, plan, emitted, out) {
471
+ const top = recordTop(ref);
472
+ const fn = goDecodeResultName(comp.name, node.id);
473
+ if (emitted.has(fn))
474
+ return fn;
475
+ emitted.add(fn);
476
+ const innerDecode = emitDecodeNamed(top.innerName, plan, emitted, out);
477
+ const resultType = renderTypeRef(ref);
478
+ const wireType = goWireTypeForTop(top);
479
+ const lines = [];
480
+ lines.push(`// ${fn} — decode the node's record-containing '${top.kind}' result from the wire (each record`);
481
+ lines.push(`// via ${innerDecode}); a de-box mismatch fails closed with the structured Error Value.`);
482
+ lines.push(`func ${fn}(wire ${wireType}) (${resultType}, error) {`);
483
+ switch (top.kind) {
484
+ case "named":
485
+ lines.push(`\treturn ${innerDecode}(wire)`);
486
+ break;
487
+ case "arr":
488
+ lines.push(`\tout := make(${resultType}, 0, len(wire))`);
489
+ lines.push(`\tfor _, w := range wire {`);
490
+ lines.push(`\t\te, err := ${innerDecode}(w)`);
491
+ lines.push(`\t\tif err != nil {`, `\t\t\treturn nil, err`, `\t\t}`);
492
+ lines.push(`\t\tout = append(out, e)`);
493
+ lines.push(`\t}`);
494
+ lines.push(`\treturn out, nil`);
495
+ break;
496
+ case "opt":
497
+ lines.push(`\tif wire == nil {`, `\t\treturn nil, nil`, `\t}`);
498
+ lines.push(`\tr, err := ${innerDecode}(wire)`);
499
+ lines.push(`\tif err != nil {`, `\t\treturn nil, err`, `\t}`);
500
+ lines.push(`\treturn &r, nil`);
501
+ break;
502
+ case "map":
503
+ lines.push(`\tout := make(${resultType})`);
504
+ lines.push(`\tfor k, w := range wire {`);
505
+ lines.push(`\t\tr, err := ${innerDecode}(w)`);
506
+ lines.push(`\t\tif err != nil {`, `\t\t\treturn nil, err`, `\t\t}`);
507
+ lines.push(`\t\tout[k] = r`);
508
+ lines.push(`\t}`);
509
+ lines.push(`\treturn out, nil`);
510
+ break;
511
+ }
512
+ lines.push(`}`);
513
+ out.push(lines.join("\n"));
514
+ return fn;
515
+ }
516
+ function emitNodeDecodeFns(comp, typedNodes, plan, emitted, out) {
517
+ for (const n of comp.body) {
518
+ if ("cond" in n)
519
+ continue;
520
+ if ("map" in n || "fanout" in n) {
521
+ const node = n;
522
+ if (!mapFanoutElemDeBoxEligible(node, typedNodes, plan))
523
+ continue;
524
+ emitDecodeElemRow(comp, node, mapFanoutElemRef(node, typedNodes, plan), plan, emitted, out);
525
+ continue;
526
+ }
527
+ const ref = typedNodes.get(n.id);
528
+ if (ref === undefined)
529
+ continue;
530
+ const top = recordTop(ref);
531
+ if (top === null)
532
+ continue;
533
+ // a NAMED top decodes in place via decodeWire_<Name>; an arr/opt/map top assembles via
534
+ // decodeWireResult_* (which itself recurses into decodeWire_<Name> per record).
535
+ if (top.kind === "named")
536
+ emitDecodeNamed(top.innerName, plan, emitted, out);
537
+ else
538
+ emitDecodeWireResult(comp, n, ref, plan, emitted, out);
539
+ }
540
+ }
541
+ /** containsNamed — whether a TypeRef transitively contains a named record (through arr / opt / map). */
542
+ function containsNamed(ref) {
543
+ switch (ref.kind) {
544
+ case "scalar":
545
+ return false;
546
+ case "named":
547
+ return true;
548
+ case "opt":
549
+ return containsNamed(ref.inner);
550
+ case "arr":
551
+ return containsNamed(ref.elem);
552
+ case "map":
553
+ return containsNamed(ref.value);
554
+ }
555
+ }
556
+ /** readsAPriorNode — a componentRef node is a DATAFLOW consumer iff at least one input port is a ref
557
+ * headed by a PRIOR BODY NODE (it reads a prior node's already-native result, #129), rather than a fresh
558
+ * external wire read whose ports come from input params / static / an element ($as) binding. This is the
559
+ * same head-is-a-body-node signal priorNodeArrElemInfo (#129) uses to lower a prior-node array feed. */
560
+ function readsAPriorNode(node, typedNodes) {
561
+ for (const expr of Object.values(node.ports ?? {})) {
562
+ const e = classifyExpr(expr);
563
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0]))
564
+ return true;
565
+ }
566
+ return false;
567
+ }
568
+ /**
569
+ * assertDeBoxableResults — fail closed (never a silent trust) for a covered node whose result would
570
+ * materialize on the trusting typed path and silently default a wrong wire value. Two shapes fail closed:
571
+ * (1) a NON-named top (arr / opt / map) that transitively contains a record the de-box does not decode
572
+ * (deeper nesting like arr(arr(named))) — it would trust the record's wire fields; (2) a HANDLER-BACKED
573
+ * componentRef node whose result is a pure scalar / scalar-collection top (arr(scalar) / opt(scalar) /
574
+ * map(scalar) / bare scalar) — it materializes `t = row.Val` on the trusting path, so a wrong wire value
575
+ * silently defaults (diverging from run_behavior's outType check). Either is a loud generation-time error
576
+ * (native-codegen-standard §2 — an unsupported shape fails closed, never escapes to a trusting path). A
577
+ * record top (de-boxed) and a DATAFLOW scalar top (a prior-node-fed result, computed from already-native
578
+ * data — #129 folds) both generate.
579
+ */
580
+ function assertDeBoxableResults(comp, typedNodes, plan) {
581
+ for (const n of comp.body) {
582
+ if ("cond" in n)
583
+ continue;
584
+ const isMapFanout = "map" in n || "fanout" in n;
585
+ const ref = isMapFanout ? mapFanoutElemRef(n, typedNodes, plan) : typedNodes.get(n.id);
586
+ const deBoxed = isMapFanout ? ref.kind === "named" : recordTop(ref) !== null;
587
+ if (deBoxed)
588
+ continue;
589
+ const nodeId = n.id;
590
+ if (containsNamed(ref)) {
591
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go 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.`);
592
+ }
593
+ // a map/fanout element is ALWAYS a per-element wire read; only a named record is de-boxed
594
+ // (decodeWireElem_*). A scalar / scalar-collection element materializes from the trusting per-element
595
+ // `.Val` — a wrong wire value silent-defaults. Fail closed.
596
+ if (isMapFanout) {
597
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go 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).`);
598
+ }
599
+ // a pure-scalar / scalar-collection top on a HANDLER-BACKED componentRef wire read trusts `t = row.Val`
600
+ // (a wrong wire value silent-defaults). A DATAFLOW consumer (a port fed by a prior node) computes from
601
+ // already-native data and generates.
602
+ if (!readsAPriorNode(n, typedNodes)) {
603
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go 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.`);
604
+ }
605
+ }
606
+ }
42
607
  function goPortFieldName(wire) {
43
608
  const safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
44
609
  const head = safe.charAt(0);
@@ -49,61 +614,43 @@ function portsStructName(compName, nodeId) {
49
614
  // type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
50
615
  return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
51
616
  }
52
- function inferPortType(node, inputPorts, asBinding) {
53
- if (staticArrElems(node) !== null)
54
- return "Value";
55
- const e = classifyExpr(node);
56
- switch (e.kind) {
57
- case "str":
58
- case "concat":
59
- return "string";
60
- case "bool":
61
- return "bool";
62
- case "ref": {
63
- // #108: a `$as`-headed ref (map element binding) — resolve the element field's scalar type.
64
- if (asBinding !== undefined && e.path[0] === asBinding.name) {
65
- try {
66
- const { ref } = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan);
67
- if (ref.kind === "scalar" && ref.scalar !== "null")
68
- return goScalarPortField(ref.scalar);
69
- }
70
- catch {
71
- return "Value";
72
- }
73
- return "Value";
74
- }
75
- // a ref to a REQUIRED scalar input port → that scalar. An OPTIONAL port has no required-scalar
76
- // kind (its native type is *T) and is lowered by the opt lane ahead of this — reaching here with
77
- // one means the shape is uncovered, and "Value" routes it to the caller's loud fail-closed.
78
- if (e.path.length === 1) {
79
- const sc = inputScalarKind(inputPorts[e.path[0]]);
80
- if (sc !== undefined)
81
- return sc;
82
- }
83
- return "Value";
84
- }
85
- default:
86
- return "Value";
87
- }
88
- }
89
617
  /** scalar TypeRef → PortFieldType (native go scalar kind). */
90
618
  function goScalarPortField(scalar) {
91
619
  return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
92
620
  }
93
- /** A static string-array port (every element a static string literal) — the graphddb `projection`
94
- * shape. Returns the list of literal strings, or null when the node is not that shape. */
95
- function staticStringArrElems(node) {
96
- const arr = staticArrElems(node);
97
- if (arr === null)
98
- return null;
99
- const out = [];
100
- for (const el of arr) {
101
- const e = classifyExpr(el);
102
- if (e.kind !== "str")
103
- return null;
104
- out.push(e.value);
621
+ /**
622
+ * goNativeResolveHead the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
623
+ * `if`/branches, a map/fanout `when` guard, AND a port. A head is a `$as` element binding, a prior body
624
+ * node's typed local, or an input port.
625
+ */
626
+ function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
627
+ return (head) => {
628
+ if (opts?.asBinding !== undefined && opts.asBase !== undefined && head === opts.asBinding.name) {
629
+ return { expr: opts.asBase, ref: opts.asBinding.ref };
630
+ }
631
+ if (isPrior(head))
632
+ return { expr: typedLocal(head), ref: typedNodes.get(head) };
633
+ return goInputHeadRef(head, comp, plan);
634
+ };
635
+ }
636
+ /**
637
+ * compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
638
+ * native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
639
+ * `ref`, its initializer the compiled `expr` — derived from a SINGLE lowering, so they cannot disagree.
640
+ * Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array, `$as` element
641
+ * field), a concat, a static array, an optional-input read — all resolve here. A port that cannot lower
642
+ * FAILS CLOSED. A FALLIBLE lowering (hoists statements) is returned as-is; only emitPortInit rejects it.
643
+ */
644
+ function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
645
+ const resolveHead = goNativeResolveHead(comp, plan, typedNodes, isPrior, opts);
646
+ try {
647
+ return new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
648
+ }
649
+ catch (e) {
650
+ if (!(e instanceof GeneratorFailure))
651
+ throw e;
652
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} does not lower to a native Go value: ${e.message}`);
105
653
  }
106
- return out;
107
654
  }
108
655
  /**
109
656
  * portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
@@ -115,96 +662,9 @@ function staticStringArrElems(node) {
115
662
  * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
116
663
  * `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
117
664
  */
118
- function portFieldGoType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
119
- // A port reading an OPTIONAL input port lowers through the shared native-expr compiler (the *T
120
- // pass-through / the coalesce default). It runs FIRST so an optional input can never fall into the
121
- // required-scalar inference below (which has no way to say "absent").
122
- const optPort = optPortCompileG(node, inputPorts, where, plan);
123
- if (optPort !== null)
124
- return renderTypeRef(optPort.ref);
125
- if (staticStringArrElems(node) !== null)
126
- return "[]string";
127
- // #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → []ElemT.
128
- const arrElem = portArrayElemRef(node, inputPorts, plan);
129
- if (arrElem !== null)
130
- return `[]${renderTypeRef(arrElem)}`;
131
- // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → []ElemT.
132
- const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
133
- if (priorArr !== null)
134
- return `[]${renderTypeRef(priorArr.elemRef)}`;
135
- const num = numLiteralGoExpr(node);
136
- if (num !== null)
137
- return Number.isInteger(node) ? "int64" : "float64";
138
- // a ref to a `$as` element field (or an input map port) whose type is a MAP or NAMED struct (NOT a bare
139
- // scalar) → the native map/struct type (#108-map: a `{map:V}` write port lowers to map[string]V).
140
- const composite = portCompositeRef(node, inputPorts, asBinding, plan);
141
- if (composite !== null)
142
- return renderTypeRef(composite);
143
- const ft = inferPortType(node, inputPorts, asBinding);
144
- if (ft === "Value") {
145
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Go type (would need a boxed dslcontracts.Value). The covered read plane is runtime-free and admits NO boxed escape. ${GO_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
146
- }
147
- return ft;
148
- }
149
- /** portCompositeRef (go twin) — the full TypeRef of a port that is a `{ref:[...]}` into a `$as` element
150
- * MAP or NAMED field (or an input MAP port with elemType). Returns null otherwise (caller falls through
151
- * to scalar/array inference). #108-map: enables a `{map:V}` write port to lower to map[string]V. */
152
- function portCompositeRef(node, inputPorts, asBinding, plan) {
153
- const e = classifyExpr(node);
154
- if (e.kind !== "ref" || plan === undefined)
155
- return null;
156
- let ref = null;
157
- if (asBinding !== undefined && e.path[0] === asBinding.name) {
158
- try {
159
- ref = goTypedInternals.typedFieldAccess(asBinding.name, asBinding.ref, e.path.slice(1), asBinding.plan).ref;
160
- }
161
- catch {
162
- return null;
163
- }
164
- }
165
- else if (e.path.length === 1) {
166
- const s = inputPorts[e.path[0]];
167
- if (s !== undefined && s.type === "map") {
168
- const et = s.elemType;
169
- if (et !== undefined)
170
- return { kind: "map", value: deriveTypeRef(et, plan) };
171
- }
172
- return null;
173
- }
174
- if (ref === null)
175
- return null;
176
- if (ref.kind === "map" || ref.kind === "named")
177
- return ref;
178
- return null;
179
- }
180
- /** emitCompositePortValue (go twin) — the OWNED native value expr for a `{map:V}`/named port ref: a typed
181
- * field access `<elemBase>.<field>` (native map/struct, ZERO boxed Value). `elemBaseExpr` is the map arm's
182
- * element base for a `$as`-headed ref (omitted at top level — only input map-port refs resolve there). */
183
- function emitCompositePortValue(node, inputPorts, asBinding, plan, elemBaseExpr) {
184
- const e = classifyExpr(node);
185
- if (e.kind !== "ref")
186
- return null;
187
- if (asBinding !== undefined && elemBaseExpr !== undefined && e.path[0] === asBinding.name) {
188
- let acc;
189
- try {
190
- acc = goTypedInternals.typedFieldAccess(elemBaseExpr, asBinding.ref, e.path.slice(1), plan);
191
- }
192
- catch {
193
- return null;
194
- }
195
- if (acc.ref.kind !== "map" && acc.ref.kind !== "named")
196
- return null;
197
- return acc.expr;
198
- }
199
- // a WHOLE-port ref to an input MAP port (with elemType) — valid at top level OR inside a map node (a
200
- // ref head that is NOT the `$as` element binding). Reads the native map[string]V off the input struct.
201
- if ((asBinding === undefined || e.path[0] !== asBinding.name) && e.path.length === 1) {
202
- const s = inputPorts[e.path[0]];
203
- if (s !== undefined && s.type === "map" && s.elemType !== undefined) {
204
- return `in.${goFieldName(e.path[0])}`;
205
- }
206
- }
207
- return null;
665
+ function portFieldGoType(node, comp, plan, typedNodes, where = "port", opts) {
666
+ const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
667
+ return c.renderedType ?? renderTypeRef(c.ref);
208
668
  }
209
669
  function staticArrElems(node) {
210
670
  if (node === null || typeof node !== "object" || Array.isArray(node))
@@ -215,36 +675,6 @@ function staticArrElems(node) {
215
675
  const arg = node.arr;
216
676
  return Array.isArray(arg) ? arg : null;
217
677
  }
218
- /**
219
- * portArrayElemRef (#110) — resolve a componentRef input port that binds a RUNTIME ARRAY (an IN-list /
220
- * array-bound WHERE head) to its native element TypeRef, or null when the port is not a covered array.
221
- * The covered shape is a SINGLE-SEGMENT, non-opt `ref` into an INPUT ARRAY port that carries a resolvable
222
- * `elemType` (scp-ir-architecture.md §5.2 / PortSchema.elemType — BC does NOT infer, consumer-interface
223
- * C3). The port then lowers to a native `[]ElemT` field fed straight from `in.<Field>` — ZERO boxed Value
224
- * on the read hot path. This REUSES the bc#108 `elemType` element lowering (deriveTypeRef over the input
225
- * port's elemType), extended from the `map…over` element-source path to a plain componentRef port.
226
- *
227
- * A static string array is a DIFFERENT covered shape ([]string, staticStringArrElems) — handled ahead of
228
- * this. An array port with NO resolvable elemType, an opt/multi-segment ref, or a non-array input schema
229
- * resolves to null here → the caller keeps its existing fail-closed behaviour (never a silent box).
230
- * Requires a TypePlan (an obj elemType maps to a named decl); without one it stays null (fail-closed).
231
- */
232
- function portArrayElemRef(node, inputPorts, plan) {
233
- if (plan === undefined)
234
- return null;
235
- if (staticStringArrElems(node) !== null)
236
- return null; // the static-string-array port ([]string) — not this shape.
237
- const e = classifyExpr(node);
238
- if (e.kind !== "ref" || e.opt || e.path.length !== 1)
239
- return null;
240
- const schema = inputPorts?.[e.path[0]];
241
- if (schema === undefined || (schema.type !== "array" && schema.type !== "arr"))
242
- return null;
243
- const et = schema.elemType;
244
- if (et === undefined)
245
- return null;
246
- return deriveTypeRef(et, plan);
247
- }
248
678
  /**
249
679
  * isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
250
680
  * (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
@@ -280,31 +710,49 @@ function numLiteralGoExpr(node) {
280
710
  // fractional / exponent literal → checked float64 (finite, already guaranteed above).
281
711
  return `${PKG}.Value(float64(${node}))`;
282
712
  }
283
- /** A port node is static (literal / ref / concat of those / static arr / bare number literal / an
284
- * optional-input read) — resolvable to a native Go value with no NewObj / no evaluate interpreter. This
285
- * is the SHAPE gate that decides native ELIGIBILITY; the lowering itself (portFieldGoType / emitPortInit)
286
- * is what proves each shape, and fails closed loudly on one it cannot lower. */
287
- function portIsStatic(node, inputPorts) {
288
- const arr = staticArrElems(node);
289
- if (arr !== null)
290
- return arr.every((el) => portIsStatic(el, inputPorts));
291
- if (numLiteralGoExpr(node) !== null)
292
- return true; // bare number literal (e.g. Query `limit`)
293
- // An optional-input read (`opt($.x)` / `coalesce(opt($.x), 20)` / `ne(opt($.x), null)`) is a
294
- // statically-resolvable port — the opt lane lowers it (or fails closed with the specific reason).
295
- if (inputPorts !== undefined && exprReadsOptionalInput(node, inputPorts))
713
+ /** A port node is static (literal / ref / concat of those / static arr / bare number literal)
714
+ * resolvable to a native Go Value with no NewObj / no evaluate interpreter. */
715
+ /** planForComp a TypePlan for a single component (its named types are component-scoped, so a
716
+ * one-component IR yields the same plan the full-IR build would for this component). */
717
+ function planForComp(comp) {
718
+ return buildTypePlan({ irVersion: 1, exprVersion: 2, components: [comp] });
719
+ }
720
+ /** buildTypedNodes the prior-node typed-local TypeRef map for a component (control-gate nodes are
721
+ * typeless; a map node carries its produced-array ref). An untyped node is omitted (not an error here). */
722
+ function buildTypedNodes(comp, plan) {
723
+ const typedNodes = new Map();
724
+ for (const n of comp.body) {
725
+ if (isControlGate(n))
726
+ continue;
727
+ try {
728
+ typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
729
+ }
730
+ catch {
731
+ /* untyped node — omit */
732
+ }
733
+ }
734
+ return typedNodes;
735
+ }
736
+ /** mapAsBinding — the `$as` element binding for a map/fanout node's element ports (the over-element
737
+ * TypeRef), or undefined when the over source does not resolve (an uncovered map). */
738
+ function mapAsBinding(comp, node, typedNodes, plan) {
739
+ try {
740
+ return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
741
+ }
742
+ catch {
743
+ return undefined;
744
+ }
745
+ }
746
+ /** portIsStatic — the ELIGIBILITY gate ASKS the one port lowering (compilePortNode / native-expr)
747
+ * whether it can lower this port, rather than re-deriving the shape rules — so the gate and the emitter
748
+ * can never disagree. A port that does not lower (or lowers fallibly) fails closed. */
749
+ function portIsStatic(node, comp, plan, typedNodes, asBinding) {
750
+ try {
751
+ compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
296
752
  return true;
297
- const e = classifyExpr(node);
298
- switch (e.kind) {
299
- case "str":
300
- case "bool":
301
- case "null":
302
- case "ref":
303
- return true;
304
- case "concat":
305
- return e.parts.every((p) => portIsStatic(reconstructG(p), inputPorts));
306
- default:
307
- return false;
753
+ }
754
+ catch {
755
+ return false;
308
756
  }
309
757
  }
310
758
  /** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
@@ -385,7 +833,7 @@ function coveredMapNode(n, bodyIds, comp) {
385
833
  // A map node must carry an outType (its ELEMENT type per the recorder SoT — authoring.ts recordMap
386
834
  // holds `node.outType = mapElem`, the PRE-array-wrap element; the emitter synthesizes the produced
387
835
  // `[]element`). A map WITHOUT outType is uncovered. Legacy hand-built IR that already carries the
388
- // `{arr:element}` produced form is ALSO accepted mapNodeArrRef normalizes both to the array ref.
836
+ // The map node's outType is the ELEMENT type; mapNodeArrRef wraps it to the produced array ref.
389
837
  const outT = n.outType;
390
838
  if (outT === undefined || typeof outT !== "object")
391
839
  return false;
@@ -405,8 +853,11 @@ function coveredMapNode(n, bodyIds, comp) {
405
853
  return false;
406
854
  }
407
855
  const ports = m.ports;
856
+ const plan = planForComp(comp);
857
+ const typedNodes = buildTypedNodes(comp, plan);
858
+ const asBinding = mapAsBinding(comp, n, typedNodes, plan);
408
859
  for (const p of Object.keys(ports))
409
- if (!portIsStatic(ports[p], comp.inputPorts))
860
+ if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
410
861
  return false;
411
862
  return true;
412
863
  }
@@ -438,8 +889,17 @@ function coveredFanoutNode(n, bodyIds, comp) {
438
889
  return false;
439
890
  }
440
891
  const ports = f.ports;
892
+ const plan = planForComp(comp);
893
+ const typedNodes = buildTypedNodes(comp, plan);
894
+ let asBinding;
895
+ try {
896
+ asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
897
+ }
898
+ catch {
899
+ asBinding = undefined;
900
+ }
441
901
  for (const p of Object.keys(ports))
442
- if (!portIsStatic(ports[p], comp.inputPorts))
902
+ if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
443
903
  return false;
444
904
  return true;
445
905
  }
@@ -460,6 +920,8 @@ function isSequentialComponentRefRead(comp) {
460
920
  * user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
461
921
  */
462
922
  function coverageRejectReason(comp) {
923
+ const crPlan = planForComp(comp);
924
+ const crTypedNodes = buildTypedNodes(comp, crPlan);
463
925
  const ep = execPlan(comp);
464
926
  if (ep === null)
465
927
  return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
@@ -546,7 +1008,7 @@ function coverageRejectReason(comp) {
546
1008
  if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
547
1009
  return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
548
1010
  for (const p of Object.keys(cr.ports))
549
- if (portIsStatic(cr.ports[p], comp.inputPorts) === false)
1011
+ if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
550
1012
  return `node '${n.id}' port '${p}' is not statically resolvable`;
551
1013
  }
552
1014
  if (!outputIsNativeLowerable(comp))
@@ -568,7 +1030,12 @@ function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
568
1030
  if (portNames.length === 0) {
569
1031
  return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
570
1032
  }
571
- const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan, typedNodes));
1033
+ // a node WITH ports always resolves through the one port lowering, which needs the type plan + the
1034
+ // prior-node map (both are supplied by every caller — narrow the optional params for the fields below).
1035
+ if (plan === undefined || typedNodes === undefined) {
1036
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native: ports struct for node '${node.id}' needs a type plan + prior-node map`);
1037
+ }
1038
+ const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}' of node '${node.id}'`, asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined));
572
1039
  const fieldNames = portNames.map(goPortFieldName);
573
1040
  const fields = portNames
574
1041
  .map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
@@ -587,7 +1054,7 @@ ${fields}
587
1054
  //
588
1055
  // The covered path no longer crosses a generic `RawValue`/`RawRow` on the RESULT plane. Each
589
1056
  // covered node has a CONCRETE row struct `RawRow_<comp>_<node>` whose fields are the node's
590
- // projected outType fields (typed, flattened) plus a per-node error signal (IsError/Err). The
1057
+ // projected outType fields (typed, flattened). A row models a SUCCESS; failure travels the (T,
591
1058
  // per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
592
1059
  // the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
593
1060
  // each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
@@ -595,7 +1062,7 @@ ${fields}
595
1062
  //
596
1063
  // A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
597
1064
  // named-struct node has one Go field per outType field (same names/types as the outType decl), so
598
- // `RawRow_<comp>_<node>` is structurally the outType struct + {IsError, Err}. The copier is then a
1065
+ // `RawRow_<comp>_<node>` is structurally the outType struct. The copier is then a
599
1066
  // mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
600
1067
  /** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
601
1068
  * prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
@@ -627,7 +1094,7 @@ function nodeMethodName(compName, nodeId) {
627
1094
  * emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
628
1095
  * covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
629
1096
  * Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
630
- * the per-node error signal (IsError/Err) so the runner interprets policy without a boxed outcome.
1097
+ * A row models a SUCCESS; failure is the second (error) return, so no in-band error signal.
631
1098
  */
632
1099
  function emitRawRowStructs(comp, typedNodes, plan) {
633
1100
  const parts = [];
@@ -642,7 +1109,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
642
1109
  const elemRowRef = fanoutItemsElemRef(n, ref, plan);
643
1110
  parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
644
1111
  parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
645
- `type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
1112
+ `type ${rawRowStructName(comp.name, n.id)} struct {\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
646
1113
  continue;
647
1114
  }
648
1115
  if ("map" in n) {
@@ -655,7 +1122,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
655
1122
  const elemRowRef = mapElemRowRef(n, arrRef, plan);
656
1123
  parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
657
1124
  parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
658
- `type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
1125
+ `type ${rawRowStructName(comp.name, n.id)} struct {\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
659
1126
  continue;
660
1127
  }
661
1128
  parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
@@ -663,13 +1130,11 @@ function emitRawRowStructs(comp, typedNodes, plan) {
663
1130
  return parts.join("\n\n");
664
1131
  }
665
1132
  /** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
666
- * always carry IsError/Err. */
1133
+ * A row models a SUCCESS; a failure is the second (error) return, so no in-band error signal. */
667
1134
  function emitOneRowStruct(name, ref, plan) {
668
1135
  const lines = [];
669
- lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
1136
+ lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType).`);
670
1137
  lines.push(`type ${name} struct {`);
671
- lines.push(`\tIsError bool`);
672
- lines.push(`\tErr string`);
673
1138
  if (ref.kind === "named") {
674
1139
  const decl = findDecl(plan, ref.name);
675
1140
  for (const f of decl.fields) {
@@ -744,21 +1209,32 @@ function emitHandlerInterface(comp, typedNodes, plan) {
744
1209
  const bt = boundGoType(n, comp, typedNodes, plan);
745
1210
  if ("fanout" in n) {
746
1211
  // fanout (v3): one batched dispatch of the deduped id list. Ports = the batch struct; returns the
747
- // aligned batch row (per-body rows) + resolved. The runner applies dedupe/drop/wrap after.
1212
+ // aligned per-body element WIRES ([]WireRow) that the runner de-boxes (decodeWireElem_*) then
1213
+ // applies dedupe/drop/wrap to. A non-record element keeps its concrete batch-row return.
748
1214
  const portsT = `${portsStructName(comp.name, n.id)}_batch`;
749
- const retT = rawRowStructName(comp.name, n.id);
750
- lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
1215
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
1216
+ const retT = elemDeBox ? "[]WireRow" : rawRowStructName(comp.name, n.id);
1217
+ lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, error)`);
751
1218
  continue;
752
1219
  }
753
1220
  if ("map" in n) {
754
1221
  const m = n.map;
755
1222
  const batched = m.batched === true;
756
1223
  const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
757
- const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
758
- lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
1224
+ // a record-element map returns the element WIRE(s) the runner de-boxes: batched → []WireRow (aligned
1225
+ // to items), nonbatched one WireRow per dispatch. A non-record element keeps its concrete row.
1226
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
1227
+ const retT = elemDeBox ? (batched ? "[]WireRow" : "WireRow") : batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
1228
+ lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, error)`);
759
1229
  continue;
760
1230
  }
761
- lines.push(`\t${method}(ports ${portsStructName(comp.name, n.id)}, bound ${bt}) (${rawRowStructName(comp.name, n.id)}, bool)`);
1231
+ // a componentRef whose result is a record-containing top returns the wire (WireRow / []WireRow /
1232
+ // nilable WireRow / map[string]WireRow); the generated de-box (decodeWireResult_*) probes each record
1233
+ // strictly. A pure-scalar top (arr(scalar)/opt(scalar)/…) keeps its concrete row for direct copy.
1234
+ const ref = typedNodes.get(n.id);
1235
+ const top = ref !== undefined ? recordTop(ref) : null;
1236
+ const retT = top !== null ? goWireTypeForTop(top) : rawRowStructName(comp.name, n.id);
1237
+ lines.push(`\t${method}(ports ${portsStructName(comp.name, n.id)}, bound ${bt}) (${retT}, error)`);
762
1238
  }
763
1239
  lines.push(`}`);
764
1240
  return lines.join("\n");
@@ -782,7 +1258,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
782
1258
  // struct element). A pure-static/concat port ignores the binding.
783
1259
  const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
784
1260
  const asBinding = { name: m.as, ref: elemRef, plan };
785
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan);
1261
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
786
1262
  const batchStruct = `${structName}_batch`;
787
1263
  return `${elemStruct}
788
1264
 
@@ -800,7 +1276,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
800
1276
  const structName = portsStructName(comp.name, node.id);
801
1277
  const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
802
1278
  const asBinding = { name: f.as, ref: elemRef, plan };
803
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan);
1279
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
804
1280
  const batchStruct = `${structName}_batch`;
805
1281
  return `${elemStruct}
806
1282
 
@@ -846,18 +1322,16 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
846
1322
  }
847
1323
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go fanout (v3): fanout '${node.id}' 'over' must be a ref to a prior node's typed arr field OR an input array port with a declared elemType (got ${JSON.stringify(f.over)}).`);
848
1324
  }
849
- /** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`, normalizing the two IR
850
- * conventions to the one array shape every downstream site assumes (`typedNodes.get(mapId)` is an arr):
851
- * - the RECORDER SoT (authoring.ts recordMap): `node.outType` is the ELEMENT (pre-array-wrap) wrap
852
- * to `{arr:element}` (the map produces `[]element`), matching type-gate's outputType wrap
853
- * (`"map" in n ? {arr:ot} : ot`). This is the declarative-authoring form (graphddb batchWrite/forEach).
854
- * - legacy hand-built IR: `node.outType` already the `{arr:element}` produced form take it as-is.
855
- * A map ELEMENT is never itself an `{arr:…}` in the covered corpus, so the discriminator is unambiguous.
856
- * Both converge to the SAME produced-array ref, so existing goldens stay byte-identical. */
1325
+ /** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`.
1326
+ *
1327
+ * A map node's `outType` is its ELEMENT type (scp-error.md "Map element type"), so the produced array
1328
+ * is `{arr: element}`. This is the same reading the compiler applies when it resolves what a node
1329
+ * result reference has (`type-gate`: `"map" in n ? {arr:ot} : ot`) the emitter follows that SSoT
1330
+ * rather than inspecting the annotation's shape, so an element that is ITSELF an `arr` stays
1331
+ * expressible (element `arr(T)` produces `arr(arr(T))`). */
857
1332
  function mapNodeArrRef(node, plan) {
858
1333
  const outT = node.outType;
859
- const isArr = outT !== null && typeof outT === "object" && "arr" in outT;
860
- return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
1334
+ return deriveTypeRef({ arr: outT }, plan);
861
1335
  }
862
1336
  /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
863
1337
  * type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
@@ -947,43 +1421,6 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
947
1421
  function mapOverElemType(comp, node, typedNodes, plan) {
948
1422
  return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
949
1423
  }
950
- /**
951
- * priorNodeArrElemInfo (#129) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE whose
952
- * annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef +
953
- * the native Go slice expression that yields it. This GENERALIZES the `map…over` prior-node-arr
954
- * dataflow (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items`
955
- * port is `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `[]ElemT`
956
- * fed straight from the prior node's typed local — ZERO boxed Value on the read hot path.
957
- *
958
- * The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
959
- * typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
960
- * if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
961
- * returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
962
- * single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
963
- * ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
964
- */
965
- function priorNodeArrElemInfo(node, typedNodes, plan) {
966
- if (typedNodes === undefined || plan === undefined)
967
- return null;
968
- if (staticArrElems(node) !== null)
969
- return null; // a static array literal — not a prior-node ref.
970
- const e = classifyExpr(node);
971
- if (e.kind !== "ref" || e.opt || e.path.length < 1)
972
- return null;
973
- if (!typedNodes.has(e.path[0]))
974
- return null; // head is not a prior body node → not this shape.
975
- const baseRef = typedNodes.get(e.path[0]);
976
- let acc;
977
- try {
978
- acc = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
979
- }
980
- catch {
981
- return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
982
- }
983
- if (acc.ref.kind !== "arr")
984
- return null; // the prior-node result (through the path) is not an array.
985
- return { elemRef: acc.ref.elem, overExpr: acc.expr };
986
- }
987
1424
  function classifyPort(node) {
988
1425
  if (staticArrElems(node) !== null)
989
1426
  return "static";
@@ -1037,31 +1474,17 @@ function reconstructG(e) {
1037
1474
  function inStructName(compName) {
1038
1475
  return `In_${sanitize(compName)}`;
1039
1476
  }
1040
- /**
1041
- * inputPortGoType — the native Go type for an input port's declared type. Scalars lower to the concrete
1042
- * scalar; `array`/`map` with a declared elemType to `[]ElemT` / `map[string]ElemT`; an OPTIONAL port
1043
- * (`{opt:T}` → `required:false`) to `*T` — the native representation of "the value is absent" (nil).
1044
- * A declared type with no native lowering stays the boxed Value.
1045
- *
1046
- * An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: a boxed `Value` cannot carry
1047
- * the presence distinction on the covered plane, and emitting the bare inner type would silently turn
1048
- * "absent" into a zero value. A port whose optionality cannot be represented is never silently coerced.
1049
- */
1477
+ /** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
1050
1478
  function inputPortGoType(schema, plan, where = "input port") {
1051
1479
  const ref = inputPortTypeRef(schema, plan);
1052
1480
  if (ref !== undefined)
1053
1481
  return renderTypeRef(ref);
1054
1482
  if (inputPortIsOptional(schema))
1055
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation (a boxed Value would be an 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.`);
1483
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} is OPTIONAL (declared {opt:…}) but its inner type ${JSON.stringify(schema?.type)} has no native Go lowering, so "absent" has no native representation. Declare an elemType for an array/map, or use a natively-lowerable scalar.`);
1056
1484
  return `${PKG}.Value`;
1057
1485
  }
1058
- /** The scalar kind a REQUIRED input port declares, or undefined when it is not a required native scalar.
1059
- * An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `*T`, and the callers of
1060
- * this function build REQUIRED scalar slots (`in.<Field>` read straight into a non-opt port field).
1061
- * Answering "int64" for an `{opt:"int"}` port is exactly how "absent" would silently become `0`. */
1486
+ /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
1062
1487
  function inputScalarKind(schema) {
1063
- if (inputPortIsOptional(schema))
1064
- return undefined;
1065
1488
  switch (schema?.type) {
1066
1489
  case "string":
1067
1490
  case "literal": // a `"literal"` union is a constrained string (see inputPortGoType).
@@ -1086,6 +1509,8 @@ function emitInStruct(comp, plan) {
1086
1509
  return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
1087
1510
  }
1088
1511
  const names = ports.map((p) => goFieldName(p));
1512
+ // #139: inputPortGoType reads the inputPortTypeRef SSoT — an OPTIONAL port is `*T` (absent → nil); an
1513
+ // optional whose inner type has no native lowering FAILS CLOSED (never a boxed Value escape).
1089
1514
  const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`));
1090
1515
  const nameW = Math.max(0, ...names.map((n) => n.length));
1091
1516
  const typeW = Math.max(0, ...types.map((t) => t.length));
@@ -1127,15 +1552,6 @@ function emitInDecoder(comp, plan) {
1127
1552
  const scalar = inputScalarKind(schema);
1128
1553
  const et = schema.elemType;
1129
1554
  lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
1130
- if (inputPortIsOptional(schema)) {
1131
- // An OPTIONAL port decodes into its native *T. materializeExpr's opt de-box IS the wire rule:
1132
- // a nil Value (null) → nil (absent), any other value → a pointer to the materialized inner. A key
1133
- // that never appears leaves the struct's zero value (nil) — the same absent value. Checked FIRST:
1134
- // optionality wraps every inner type (an optional array is *[]T, not []T).
1135
- lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
1136
- lines.push(`\t}`);
1137
- continue;
1138
- }
1139
1555
  if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
1140
1556
  // #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
1141
1557
  // via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
@@ -1166,11 +1582,9 @@ function emitInDecoder(comp, plan) {
1166
1582
  lines.push(`\t\tin.${field} = v`);
1167
1583
  }
1168
1584
  else {
1169
- lines.push(`\t\tt, tok := v.(${scalar})`);
1170
- lines.push(`\t\tif !tok {`);
1171
- lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected ${scalar}, got "+${PKG}.TypeName(v))`);
1172
- lines.push(`\t\t}`);
1173
- lines.push(`\t\tin.${field} = t`);
1585
+ // #139: materialize via the inputPortTypeRef SSoT so an OPTIONAL scalar reads as *T (present &v);
1586
+ // an absent optional never enters this block (Get !ok), leaving the field nil (not a required zero).
1587
+ lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
1174
1588
  }
1175
1589
  lines.push(`\t}`);
1176
1590
  }
@@ -1178,53 +1592,10 @@ function emitInDecoder(comp, plan) {
1178
1592
  lines.push(`}`);
1179
1593
  return lines.join("\n");
1180
1594
  }
1181
- /** The port forms the STATIC (non-optional) lane lowers named in its reject message so the message
1182
- * describes what is actually covered rather than a stale corpus list. */
1183
- const GO_STATIC_PORT_FORMS = "This port does not read an optional input, so it lowers through the STATIC port forms: a string / bool / number literal, a static string-array (projection), a ref to a REQUIRED scalar input port, a ref to an input array/map port or a prior node's array (declared elemType), a ref to a `$as` element field, or a concat of native strings. A dynamic operator at port position is not covered on this lane. (A port that DOES read an optional input lowers through the full native expression compiler, which admits the whole closed operator set — so the covered operator set at port position currently depends on the lane, not on the operator.)";
1184
- /**
1185
- * emitNativeScalar — lower a static port expr to a NATIVE Go expression of the concrete `want` scalar
1186
- * type, or null if it cannot be statically proven that type. Literals: a string literal is native for
1187
- * `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef` (typed input
1188
- * field / prior-node scalar / element binding). A concat lowers ONLY into `string` and only when EVERY
1189
- * part is itself a native string (so the concat's strings-only TYPE_MISMATCH can never fire — the parts
1190
- * are statically strings), emitting `p0 + p1 + …`. Returns null (fall back to the Value path) otherwise.
1191
- */
1192
- function emitNativeScalar(node, want, resolveRef) {
1193
- // a static arr is a Value port (never a scalar) — not native-lowerable here.
1194
- if (staticArrElems(node) !== null)
1195
- return null;
1196
- const e = classifyExpr(node);
1197
- switch (e.kind) {
1198
- case "str":
1199
- return want === "string" ? JSON.stringify(e.value) : null;
1200
- case "bool":
1201
- return want === "bool" ? (e.value ? "true" : "false") : null;
1202
- case "null":
1203
- return null; // a null literal is a Value(nil), never a required native scalar
1204
- case "ref": {
1205
- const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
1206
- if (sc === null || sc.scalar !== want)
1207
- return null;
1208
- return sc.expr;
1209
- }
1210
- case "concat": {
1211
- if (want !== "string")
1212
- return null;
1213
- const parts = [];
1214
- for (const p of e.parts) {
1215
- const sub = emitNativeScalar(reconstructG(p), "string", resolveRef);
1216
- if (sub === null)
1217
- return null;
1218
- // parenthesize a `+` sub-expression so string concatenation binds correctly (a ref/literal is
1219
- // atomic; a nested concat is already a single `a + b` — wrap to keep it one operand).
1220
- parts.push(sub.includes(" + ") ? `(${sub})` : sub);
1221
- }
1222
- return parts.join(" + ");
1223
- }
1224
- default:
1225
- return null;
1226
- }
1227
- }
1595
+ /** A ref resolver for native lowering: given a ref head + remaining field path, return a native scalar
1596
+ * Go expression (typed input field / prior-node typed struct scalar / element binding field), or null
1597
+ * when the ref does not resolve to a REQUIRED (non-opt) native scalar (then the caller falls back to
1598
+ * the nvVE Value path — which keeps the runtime null/opt/TYPE_MISMATCH semantics). */
1228
1599
  /**
1229
1600
  * emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
1230
1601
  * runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
@@ -1236,66 +1607,11 @@ function emitNativeScalar(node, want, resolveRef) {
1236
1607
  * plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
1237
1608
  * struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
1238
1609
  */
1239
- function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typedNodes, asBinding, elemBaseExpr) {
1240
- const fieldName = goPortFieldName(pn);
1241
- // The OPTIONAL-input lane (*T pass-through / coalesce default) — same shared compile as
1242
- // portFieldGoType, so the field type and its initializer are derived from ONE lowering.
1243
- const optPort = optPortCompileG(expr, inputPorts ?? {}, `port '${pn}'`, plan);
1244
- if (optPort !== null)
1245
- return `${fieldName}: ${optPort.expr}`;
1246
- // #108-map: a port that is a ref to a `$as` element MAP/NAMED field (or an input map port) → the typed
1247
- // composite value directly off the element/input (native map[string]V / struct — ZERO boxed Value).
1248
- if (plan !== undefined) {
1249
- const compExpr = emitCompositePortValue(expr, inputPorts ?? {}, asBinding, plan, elemBaseExpr);
1250
- if (compExpr !== null)
1251
- return `${fieldName}: ${compExpr}`;
1252
- }
1253
- // #110: a componentRef port bound to a runtime array input port (with elemType) → the native []ElemT,
1254
- // read straight from the concrete input struct (`in.<Field>`) — ZERO boxed Value on the read hot path.
1255
- // The over-elem/element-source case (map…over) already resolves via mapOverElemInfo; this is the plain
1256
- // componentRef port (an IN-list / array-bound WHERE head).
1257
- if (inputPorts !== undefined && portArrayElemRef(expr, inputPorts, plan) !== null) {
1258
- const e = classifyExpr(expr);
1259
- if (e.kind === "ref" && e.path.length === 1) {
1260
- return `${fieldName}: in.${goFieldName(e.path[0])}`;
1261
- }
1262
- }
1263
- // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
1264
- // the native []ElemT read straight off the prior node's typed local (`t_<node>.Field…`) — ZERO boxed
1265
- // Value on the read hot path. The over-source case (map…over) resolves via mapOverElemInfo; this is
1266
- // the plain leaf array input port (a fold/dedup leaf whose `items` = a prior node's array result).
1267
- const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
1268
- if (priorArr !== null)
1269
- return `${fieldName}: ${priorArr.overExpr}`;
1270
- // change #2: a static string array (projection) → a native `[]string{...}` literal.
1271
- const strArr = staticStringArrElems(expr);
1272
- if (strArr !== null) {
1273
- if (fieldGoType !== "[]string") {
1274
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldGoType}' (expected []string).`);
1275
- }
1276
- return `${fieldName}: []string{${strArr.map((s) => JSON.stringify(s)).join(", ")}}`;
1277
- }
1278
- // a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralGoExpr —
1279
- // emit the concrete native int64/float64 (unwrap the dslcontracts.Value(...) box: it was only there
1280
- // for the old boxed path; the range check is what matters, and it already passed).
1281
- const num = numLiteralGoExpr(expr);
1282
- if (num !== null) {
1283
- if (fieldGoType === "int64")
1284
- return `${fieldName}: int64(${expr})`;
1285
- if (fieldGoType === "float64")
1286
- return `${fieldName}: float64(${expr})`;
1287
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldGoType}'.`);
1288
- }
1289
- // string / bool / scalar-input / concat → native scalar expression (no Value, no `.(T)`).
1290
- const scalarWant = fieldGoType;
1291
- const native = emitNativeScalar(expr, scalarWant, resolveRef);
1292
- if (native !== null) {
1293
- return `${fieldName}: ${native}`;
1294
- }
1295
- // FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
1296
- // re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
1297
- // component is not native-eligible and must regenerate on the boxed straight-line path.
1298
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Go value of type '${fieldGoType}'. The covered read plane admits NO boxed dslcontracts.Value escape. ${GO_STATIC_PORT_FORMS} Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
1610
+ function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
1611
+ const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
1612
+ if (c.stmts.length > 0)
1613
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go 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. Use a non-fallible port expression.`);
1614
+ return `${goPortFieldName(pn)}: ${c.expr}`;
1299
1615
  }
1300
1616
  /**
1301
1617
  * emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
@@ -1318,7 +1634,7 @@ function emitPortInit(pn, expr, fieldGoType, resolveRef, inputPorts, plan, typed
1318
1634
  *
1319
1635
  * NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
1320
1636
  */
1321
- function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt) {
1637
+ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags) {
1322
1638
  const lines = [];
1323
1639
  const ids = stage.map((i) => comp.body[i].id);
1324
1640
  lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
@@ -1355,9 +1671,17 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1355
1671
  const node = comp.body[i];
1356
1672
  const s = sanitize(node.id);
1357
1673
  const structName = portsStructName(comp.name, comp.body[i].id);
1358
- const rowT = rawRowStructName(comp.name, comp.body[i].id);
1359
- lines.push(`\t\tvar row_${s} ${rowT}`);
1360
- lines.push(`\t\tvar row_${s}Resolved bool`);
1674
+ const memRef = typedNodes.get(comp.body[i].id);
1675
+ // a de-box member's goroutine returns the top's wire (WireRow / []WireRow / nilable WireRow /
1676
+ // map[string]WireRow, decoded in COMMIT); a non-de-box member returns its concrete row directly.
1677
+ const memTop = recordTop(memRef);
1678
+ if (memTop !== null) {
1679
+ lines.push(`\t\tvar wire_${s} ${goWireTypeForTop(memTop)}`);
1680
+ }
1681
+ else {
1682
+ lines.push(`\t\tvar row_${s} ${rawRowStructName(comp.name, comp.body[i].id)}`);
1683
+ }
1684
+ lines.push(`\t\tvar row_${s}Err error`);
1361
1685
  lines.push(`\t\trun_${s} := false`);
1362
1686
  lines.push(`\t\tvar ps_${s} ${structName}`);
1363
1687
  lines.push(`\t\t_ = ps_${s}`);
@@ -1407,11 +1731,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1407
1731
  }
1408
1732
  }
1409
1733
  const inits = [];
1410
- const resolveRef = resolveRefAt(atPos);
1411
1734
  for (const pn of Object.keys(node.ports)) {
1412
- const expr = node.ports[pn];
1413
- const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1414
- inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan, typedNodes));
1735
+ inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, atPos)));
1415
1736
  }
1416
1737
  lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
1417
1738
  lines.push(`${ind}run_${s} = true`);
@@ -1432,7 +1753,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1432
1753
  lines.push(`\t\t\t\tdefer wg.Done()`);
1433
1754
  lines.push(`\t\t\t\tdefer func() { <-sem }()`);
1434
1755
  const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
1435
- lines.push(`\t\t\t\trow_${s}, row_${s}Resolved = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, ${boundArg})`);
1756
+ const resultVar = deBoxEligible(typedNodes.get(node.id)) ? `wire_${s}` : `row_${s}`;
1757
+ lines.push(`\t\t\t\t${resultVar}, row_${s}Err = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, ${boundArg})`);
1436
1758
  lines.push(`\t\t\t}()`);
1437
1759
  lines.push(`\t\t}`);
1438
1760
  }
@@ -1444,20 +1766,53 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1444
1766
  const s = sanitize(node.id);
1445
1767
  const ref = typedNodes.get(node.id);
1446
1768
  const oc = `row_${s}`;
1769
+ const top = recordTop(ref);
1770
+ const namedTop = top !== null && top.kind === "named";
1771
+ const deBox = top !== null;
1772
+ // a NAMED top decodes in place (decodeWire_<Name> → oc → field copy); an arr/opt/map top assembles
1773
+ // via decodeWireResult_* (the same decode recursion, one per record) into the typed local directly.
1447
1774
  lines.push(`\t\tif run_${s} {`);
1448
- lines.push(`\t\t\tif !${oc}Resolved {`, `\t\t\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `\t\t\t}`);
1449
1775
  if (meta.policy === "continue") {
1450
- lines.push(`\t\t\tif !${oc}.IsError {`);
1451
- lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 4));
1452
- lines.push(`\t\t\t\tproduced_${s} = true`);
1776
+ lines.push(`\t\t\tif ${oc}Err == nil {`);
1777
+ if (namedTop) {
1778
+ lines.push(`\t\t\t\tif ${oc}, ${oc}DErr := ${goDecodeNamedName(top.innerName)}(wire_${s}); ${oc}DErr == nil {`);
1779
+ lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 5));
1780
+ lines.push(`\t\t\t\t\tproduced_${s} = true`);
1781
+ lines.push(`\t\t\t\t}`);
1782
+ }
1783
+ else if (deBox) {
1784
+ lines.push(`\t\t\t\tif res_${s}, res_${s}DErr := ${goDecodeResultName(comp.name, node.id)}(wire_${s}); res_${s}DErr == nil {`);
1785
+ lines.push(`\t\t\t\t\t${typedLocal(node.id)} = res_${s}`);
1786
+ lines.push(`\t\t\t\t\tproduced_${s} = true`);
1787
+ lines.push(`\t\t\t\t}`);
1788
+ }
1789
+ else {
1790
+ lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 4));
1791
+ lines.push(`\t\t\t\tproduced_${s} = true`);
1792
+ }
1453
1793
  lines.push(`\t\t\t}`);
1454
1794
  }
1455
1795
  else {
1456
- const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1457
- lines.push(`\t\t\tif ${oc}.IsError {`);
1458
- lines.push(`\t\t\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
1796
+ lines.push(`\t\t\tif ${oc}Err != nil {`);
1797
+ lines.push(`\t\t\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${JSON.stringify(meta.policy === "retry" ? "retry" : "fail")}, ${oc}Err)`);
1459
1798
  lines.push(`\t\t\t}`);
1460
- lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 3));
1799
+ if (namedTop) {
1800
+ lines.push(`\t\t\t${oc}, ${oc}DErr := ${goDecodeNamedName(top.innerName)}(wire_${s})`);
1801
+ lines.push(`\t\t\tif ${oc}DErr != nil {`);
1802
+ lines.push(`\t\t\t\treturn ${zeroOut}, ${oc}DErr`);
1803
+ lines.push(`\t\t\t}`);
1804
+ lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 3));
1805
+ }
1806
+ else if (deBox) {
1807
+ lines.push(`\t\t\tres_${s}, res_${s}DErr := ${goDecodeResultName(comp.name, node.id)}(wire_${s})`);
1808
+ lines.push(`\t\t\tif res_${s}DErr != nil {`);
1809
+ lines.push(`\t\t\t\treturn ${zeroOut}, res_${s}DErr`);
1810
+ lines.push(`\t\t\t}`);
1811
+ lines.push(`\t\t\t${typedLocal(node.id)} = res_${s}`);
1812
+ }
1813
+ else {
1814
+ lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 3));
1815
+ }
1461
1816
  lines.push(`\t\t\tproduced_${s} = true`);
1462
1817
  }
1463
1818
  lines.push(`\t\t}`);
@@ -1518,35 +1873,6 @@ function emitNativeRawRunner(comp, plan, flags) {
1518
1873
  // assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
1519
1874
  lines.push(`func ${runnerName(comp.name)}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
1520
1875
  lines.push(`\t_ = in`);
1521
- // resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
1522
- // directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
1523
- // Go expression; an opt / non-scalar / refOpt resolves to null (falls back to the nvVE Value path).
1524
- const resolveRefAt = (atPos) => (head, restPath, opt) => {
1525
- if (opt)
1526
- return null; // a refOpt may be nil at runtime — keep the Value path (opt-nil semantics)
1527
- if (priorNodeAt(head, atPos)) {
1528
- const baseRef = typedNodes.get(head);
1529
- let acc;
1530
- let expr;
1531
- try {
1532
- const r = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, restPath, plan);
1533
- expr = r.expr;
1534
- acc = r.ref;
1535
- }
1536
- catch {
1537
- return null;
1538
- }
1539
- if (acc.kind !== "scalar" || acc.scalar === "null")
1540
- return null;
1541
- return { expr, scalar: goScalar(acc.scalar) };
1542
- }
1543
- if (restPath.length !== 0)
1544
- return null; // input.<field> path is a Value walk — not a native scalar
1545
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1546
- if (scalar === undefined)
1547
- return null;
1548
- return { expr: `in.${goFieldName(head)}`, scalar };
1549
- };
1550
1876
  for (const k of order.keys()) {
1551
1877
  const node = comp.body[order[k]];
1552
1878
  const id = node.id;
@@ -1569,7 +1895,7 @@ function emitNativeRawRunner(comp, plan, flags) {
1569
1895
  // ascending-sorted stages), so we emit the block at the first member and skip the rest.
1570
1896
  const stage = ep.parallelStageOf.get(i);
1571
1897
  if (stage !== undefined && stage[0] === i) {
1572
- lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt));
1898
+ lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags));
1573
1899
  k += stage.length - 1;
1574
1900
  continue;
1575
1901
  }
@@ -1641,30 +1967,80 @@ function emitNativeRawRunner(comp, plan, flags) {
1641
1967
  // dslcontracts.Value, ZERO nvVE helper, ZERO `.(T)`. A genuinely-dynamic port FAILS CLOSED.
1642
1968
  const inits = [];
1643
1969
  const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
1644
- const resolveRef = resolveRefAt(k);
1645
1970
  for (const pn of portNames) {
1646
- const expr = node.ports[pn];
1647
- const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1648
- inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef, comp.inputPorts, plan, typedNodes));
1971
+ inits.push(emitPortInit(pn, node.ports[pn], comp, plan, typedNodes, (h) => priorNodeAt(h, k)));
1649
1972
  }
1650
1973
  lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
1651
1974
  const oc = `row_${s}`;
1652
- lines.push(`${indent}${oc}, ${oc}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
1653
- lines.push(`${indent}if !${oc}Resolved {`, `${indent}\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `${indent}}`);
1654
1975
  const copy = (ind) => emitRowCopy(typedLocal(node.id), oc, ref, plan, ind.length);
1976
+ // a NAMED top returns a WireRow, decoded in place (decodeWire_<Name> → oc → field copy). An arr/opt/map
1977
+ // top returns []WireRow / nilable WireRow / map[string]WireRow, assembled via decodeWireResult_* (the same
1978
+ // decode recursion, one per record). Either way a de-box mismatch fails closed with the structured Error
1979
+ // Value (byte-equal codes to run_behavior's outType check, so ≡ holds). A non-de-box node keeps its
1980
+ // direct concrete-row return + field copy.
1981
+ const top = recordTop(ref);
1982
+ const namedTop = top !== null && top.kind === "named";
1983
+ const deBox = top !== null;
1655
1984
  if (meta.policy === "continue") {
1656
- lines.push(`${indent}if !${oc}.IsError {`);
1657
- lines.push(copy(indent + "\t"));
1658
- lines.push(`${indent}\tproduced_${s} = true`);
1659
- lines.push(`${indent}}`);
1985
+ // continue: a failed node (leaf failure OR de-box mismatch) produces no Port — the downstream
1986
+ // Skip propagation is what `continue` means. Only a clean decode commits the result.
1987
+ if (namedTop) {
1988
+ lines.push(`${indent}if wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg}); wire_${s}Err == nil {`);
1989
+ lines.push(`${indent}\tif ${oc}, ${oc}DErr := ${goDecodeNamedName(top.innerName)}(wire_${s}); ${oc}DErr == nil {`);
1990
+ lines.push(copy(indent + "\t\t"));
1991
+ lines.push(`${indent}\t\tproduced_${s} = true`);
1992
+ lines.push(`${indent}\t}`);
1993
+ lines.push(`${indent}}`);
1994
+ }
1995
+ else if (deBox) {
1996
+ lines.push(`${indent}if wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg}); wire_${s}Err == nil {`);
1997
+ lines.push(`${indent}\tif res_${s}, res_${s}DErr := ${goDecodeResultName(comp.name, node.id)}(wire_${s}); res_${s}DErr == nil {`);
1998
+ lines.push(`${indent}\t\t${typedLocal(node.id)} = res_${s}`);
1999
+ lines.push(`${indent}\t\tproduced_${s} = true`);
2000
+ lines.push(`${indent}\t}`);
2001
+ lines.push(`${indent}}`);
2002
+ }
2003
+ else {
2004
+ lines.push(`${indent}if ${oc}, ${oc}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg}); ${oc}Err == nil {`);
2005
+ lines.push(copy(indent + "\t"));
2006
+ lines.push(`${indent}\tproduced_${s} = true`);
2007
+ lines.push(`${indent}}`);
2008
+ }
1660
2009
  }
1661
2010
  else {
1662
- const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1663
- lines.push(`${indent}if ${oc}.IsError {`);
1664
- lines.push(`${indent}\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
1665
- lines.push(`${indent}}`);
1666
- lines.push(copy(indent));
1667
- lines.push(`${indent}produced_${s} = true`);
2011
+ const policyLit = JSON.stringify(meta.policy === "retry" ? "retry" : "fail");
2012
+ if (namedTop) {
2013
+ lines.push(`${indent}wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
2014
+ lines.push(`${indent}if wire_${s}Err != nil {`);
2015
+ lines.push(`${indent}\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${policyLit}, wire_${s}Err)`);
2016
+ lines.push(`${indent}}`);
2017
+ lines.push(`${indent}${oc}, ${oc}DErr := ${goDecodeNamedName(top.innerName)}(wire_${s})`);
2018
+ lines.push(`${indent}if ${oc}DErr != nil {`);
2019
+ lines.push(`${indent}\treturn ${zeroOut}, ${oc}DErr`);
2020
+ lines.push(`${indent}}`);
2021
+ lines.push(copy(indent));
2022
+ lines.push(`${indent}produced_${s} = true`);
2023
+ }
2024
+ else if (deBox) {
2025
+ lines.push(`${indent}wire_${s}, wire_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
2026
+ lines.push(`${indent}if wire_${s}Err != nil {`);
2027
+ lines.push(`${indent}\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${policyLit}, wire_${s}Err)`);
2028
+ lines.push(`${indent}}`);
2029
+ lines.push(`${indent}res_${s}, res_${s}DErr := ${goDecodeResultName(comp.name, node.id)}(wire_${s})`);
2030
+ lines.push(`${indent}if res_${s}DErr != nil {`);
2031
+ lines.push(`${indent}\treturn ${zeroOut}, res_${s}DErr`);
2032
+ lines.push(`${indent}}`);
2033
+ lines.push(`${indent}${typedLocal(node.id)} = res_${s}`);
2034
+ lines.push(`${indent}produced_${s} = true`);
2035
+ }
2036
+ else {
2037
+ lines.push(`${indent}${oc}, ${oc}Err := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
2038
+ lines.push(`${indent}if ${oc}Err != nil {`);
2039
+ lines.push(`${indent}\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, ${policyLit}, ${oc}Err)`);
2040
+ lines.push(`${indent}}`);
2041
+ lines.push(copy(indent));
2042
+ lines.push(`${indent}produced_${s} = true`);
2043
+ }
1668
2044
  }
1669
2045
  for (const c of closers)
1670
2046
  lines.push(c);
@@ -1738,67 +2114,16 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1738
2114
  const mkGuard = (elemVar) => {
1739
2115
  if (!hasGuard)
1740
2116
  return null;
1741
- const resolveHead = (head) => {
1742
- if (head === asName)
1743
- return { expr: elemVar, ref: overElemRef };
1744
- if (priorHere(head))
1745
- return { expr: typedLocal(head), ref: typedNodes.get(head) };
1746
- return goInputHeadRef(head, comp, plan);
1747
- };
2117
+ const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: elemVar });
1748
2118
  const be = makeGoExprBackend(zeroOut, resolveHead, plan);
1749
2119
  return new NativeExprCompiler(be).compileBool(m.when);
1750
2120
  };
1751
2121
  // emit the per-element native ports struct build (shared; `il` is the indent inside the element
1752
2122
  // loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
1753
2123
  const buildPorts = (il, elemVar) => {
1754
- const out = [];
1755
- const inits = [];
1756
- // resolveNativeRef for a map element port: a ref head is the element binding ($as → the over element
1757
- // typed struct field `elemVar.<Field>`), a PRIOR NODE (its typed struct field), or an INPUT PORT
1758
- // (`in.<Field>`). A REQUIRED scalar lowers to a native Go expr; opt / non-scalar → null (Value path).
1759
- const resolveRef = (head, restPath, opt) => {
1760
- if (opt)
1761
- return null;
1762
- let baseExpr;
1763
- let baseRef;
1764
- if (head === asName) {
1765
- baseExpr = elemVar;
1766
- baseRef = overElemRef;
1767
- }
1768
- else if (priorHere(head)) {
1769
- baseExpr = typedLocal(head);
1770
- baseRef = typedNodes.get(head);
1771
- }
1772
- else {
1773
- if (restPath.length !== 0)
1774
- return null;
1775
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1776
- if (scalar === undefined)
1777
- return null;
1778
- return { expr: `in.${goFieldName(head)}`, scalar };
1779
- }
1780
- let acc;
1781
- let expr;
1782
- try {
1783
- const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
1784
- expr = r.expr;
1785
- acc = r.ref;
1786
- }
1787
- catch {
1788
- return null;
1789
- }
1790
- if (acc.kind !== "scalar" || acc.scalar === "null")
1791
- return null;
1792
- return { expr, scalar: goScalar(acc.scalar) };
1793
- };
1794
2124
  const asBinding = { name: asName, ref: overElemRef, plan };
1795
- for (const pn of portNames) {
1796
- const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`, asBinding, plan);
1797
- // #108-map: pass the element binding + base expr so a `$as` MAP/NAMED field port reads the typed composite.
1798
- inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan, undefined, asBinding, elemLocal(s)));
1799
- }
1800
- out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
1801
- return out;
2125
+ const inits = portNames.map((pn) => emitPortInit(pn, m.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
2126
+ return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
1802
2127
  };
1803
2128
  const lines = [];
1804
2129
  const closers = [];
@@ -1814,6 +2139,12 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1814
2139
  lines.push(`${indent}over_${s} := ${overExpr}`);
1815
2140
  lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
1816
2141
  const elemRowRef = mapElemRowRef(node, arrRef, plan);
2142
+ // a record element is de-boxed strictly: the handler returns the element WIRE and the arm decodes it via
2143
+ // decodeWireElem_* (closing the scan-row silent-default hole). A de-box mismatch fails closed regardless
2144
+ // of elementPolicy (matching the interpreter's whole-node outType throw); elementPolicy still governs the
2145
+ // handler's own leaf failures only.
2146
+ const elemDeBox = elemRowRef.kind === "named";
2147
+ const decElemFn = elemDeBox ? goDecodeElemName(comp.name, node.id) : "";
1817
2148
  // emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
1818
2149
  // On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
1819
2150
  const emitGuard = (il, elemVar) => {
@@ -1830,7 +2161,7 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1830
2161
  };
1831
2162
  if (batched) {
1832
2163
  // BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
1833
- // dispatch ONCE. The concrete Node_* returns RawRow{IsError,Err,Rows []RawElem} aligned to items.
2164
+ // dispatch ONCE. The concrete Node_* returns RawRow{Rows []RawElem} aligned to items (or an error).
1834
2165
  // With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
1835
2166
  // keptIdx-aligned batch. `into` is never combined with a guard (rejected). no-into collects each row.
1836
2167
  lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
@@ -1848,22 +2179,39 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1848
2179
  // empty items => empty result (handler not called), matching run_behavior.
1849
2180
  lines.push(`${indent}if len(items_${s}) > 0 {`);
1850
2181
  lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
1851
- lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
1852
- lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1853
- lines.push(`${indent}\tif mo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+mo_${s}.Err`)}`, `${indent}\t}`);
1854
- lines.push(`${indent}\tif len(mo_${s}.Rows) != len(items_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("MAP_BATCH_RESULT_MISMATCH", `fmt.Sprintf("map '${node.id}': batched handler must return a list aligned to items (want %d)", len(items_${s}))`)}`, `${indent}\t}`);
2182
+ lines.push(`${indent}\tmo_${s}, mo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
2183
+ lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", mo_${s}Err)`, `${indent}\t}`);
2184
+ // the batch length aligns to items (the handler returns the element WIRES aligned; a record element
2185
+ // is de-boxed below, a non-record element is the concrete batch row).
2186
+ const batchLen = elemDeBox ? `len(mo_${s})` : `len(mo_${s}.Rows)`;
2187
+ const batchAt = (mk) => (elemDeBox ? `mo_${s}[${mk}]` : `mo_${s}.Rows[${mk}]`);
2188
+ lines.push(`${indent}\tif ${batchLen} != len(items_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("MAP_BATCH_RESULT_MISMATCH", `fmt.Sprintf("map '${node.id}': batched handler must return a list aligned to items (want %d)", len(items_${s}))`)}`, `${indent}\t}`);
1855
2189
  if (hasInto) {
1856
2190
  // #86 pt2: zip-attach the into field onto each kept over element (augmented element copier).
1857
2191
  lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
1858
- lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], mo_${s}.Rows[mk_${s}])`);
2192
+ if (elemDeBox) {
2193
+ lines.push(`${indent}\t\telm_${s}, elm_${s}Err := ${decElemFn}(${batchAt(`mk_${s}`)})`);
2194
+ lines.push(`${indent}\t\tif elm_${s}Err != nil {`, `${indent}\t\t\treturn ${zeroOut}, elm_${s}Err`, `${indent}\t\t}`);
2195
+ lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], elm_${s})`);
2196
+ }
2197
+ else {
2198
+ lines.push(`${indent}\t\tel_${s} := ${elemMat}(keptOver_${s}[mk_${s}], ${batchAt(`mk_${s}`)})`);
2199
+ }
1859
2200
  lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1860
2201
  lines.push(`${indent}\t}`);
1861
2202
  }
1862
2203
  else {
1863
2204
  // #93 shape 3 / #108: collect each element DIRECTLY from the aligned per-element row (field copy).
1864
- lines.push(`${indent}\tfor mk_${s} := range mo_${s}.Rows {`);
2205
+ lines.push(`${indent}\tfor mk_${s} := 0; mk_${s} < ${batchLen}; mk_${s}++ {`);
1865
2206
  lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1866
- lines.push(emitRowCopy(`el_${s}`, `mo_${s}.Rows[mk_${s}]`, elemRowRef, plan, indent.length + 2));
2207
+ if (elemDeBox) {
2208
+ lines.push(`${indent}\t\telm_${s}, elm_${s}Err := ${decElemFn}(${batchAt(`mk_${s}`)})`);
2209
+ lines.push(`${indent}\t\tif elm_${s}Err != nil {`, `${indent}\t\t\treturn ${zeroOut}, elm_${s}Err`, `${indent}\t\t}`);
2210
+ lines.push(emitRowCopy(`el_${s}`, `elm_${s}`, elemRowRef, plan, indent.length + 2));
2211
+ }
2212
+ else {
2213
+ lines.push(emitRowCopy(`el_${s}`, batchAt(`mk_${s}`), elemRowRef, plan, indent.length + 2));
2214
+ }
1867
2215
  lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1868
2216
  lines.push(`${indent}\t}`);
1869
2217
  }
@@ -1882,15 +2230,30 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1882
2230
  // per-element dispatch (ONE physical request per element). run_behavior passes the over element
1883
2231
  // as the bound value; the element data is fully conveyed by the native ports struct here, so bound
1884
2232
  // stays nil. Byte-equivalence holds because the bound value only feeds handler-internal logic.
1885
- lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ep_${s}, nil)`);
1886
- lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1887
- lines.push(`${indent}\tif mo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+mo_${s}.Err`)}`, `${indent}\t}`);
2233
+ // Element Error Policy (scp-error.md): `skip` drops the failing element and proceeds (order is
2234
+ // preserved; the leaf keeps the Error Value it produced). `error` (default) promotes the element
2235
+ // Failure to the map's Component Failure, carrying the leaf's detail through.
2236
+ lines.push(`${indent}\tmo_${s}, mo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(ep_${s}, nil)`);
2237
+ if (m.elementPolicy === "skip") {
2238
+ lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\tcontinue`, `${indent}\t}`);
2239
+ }
2240
+ else {
2241
+ lines.push(`${indent}\tif mo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", mo_${s}Err)`, `${indent}\t}`);
2242
+ }
2243
+ // a record element's wire is de-boxed strictly. A de-box mismatch fails closed regardless of
2244
+ // elementPolicy (≡ the interpreter's whole-node outType throw); elementPolicy governs the handler's
2245
+ // own leaf failures only (above).
2246
+ const elemVar = elemDeBox ? `elm_${s}` : `mo_${s}`;
2247
+ if (elemDeBox) {
2248
+ lines.push(`${indent}\telm_${s}, elm_${s}Err := ${decElemFn}(mo_${s})`);
2249
+ lines.push(`${indent}\tif elm_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, elm_${s}Err`, `${indent}\t}`);
2250
+ }
1888
2251
  if (hasInto) {
1889
- lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, mo_${s})`);
2252
+ lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, ${elemVar})`);
1890
2253
  }
1891
2254
  else {
1892
2255
  lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1893
- lines.push(emitRowCopy(`el_${s}`, `mo_${s}`, elemRowRef, plan, indent.length + 1));
2256
+ lines.push(emitRowCopy(`el_${s}`, elemVar, elemRowRef, plan, indent.length + 1));
1894
2257
  }
1895
2258
  lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1896
2259
  lines.push(`${indent}}`);
@@ -1936,47 +2299,8 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
1936
2299
  const dkGo = goFieldName(f.dedupeKey);
1937
2300
  // per-id native ports build (same as a batched map's per-element ports). $as → the over id value.
1938
2301
  const buildPorts = (il, elemVar) => {
1939
- const inits = [];
1940
- const resolveRef = (head, restPath, opt) => {
1941
- if (opt)
1942
- return null;
1943
- let baseExpr;
1944
- let baseRef;
1945
- if (head === asName) {
1946
- baseExpr = elemVar;
1947
- baseRef = overElemRef;
1948
- }
1949
- else if (priorHere(head)) {
1950
- baseExpr = typedLocal(head);
1951
- baseRef = typedNodes.get(head);
1952
- }
1953
- else {
1954
- if (restPath.length !== 0)
1955
- return null;
1956
- const scalar = inputScalarKind(comp.inputPorts?.[head]);
1957
- if (scalar === undefined)
1958
- return null;
1959
- return { expr: `in.${goFieldName(head)}`, scalar };
1960
- }
1961
- let acc;
1962
- let expr;
1963
- try {
1964
- const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
1965
- expr = r.expr;
1966
- acc = r.ref;
1967
- }
1968
- catch {
1969
- return null;
1970
- }
1971
- if (acc.kind !== "scalar" || acc.scalar === "null")
1972
- return null;
1973
- return { expr, scalar: goScalar(acc.scalar) };
1974
- };
1975
2302
  const asBinding = { name: asName, ref: overElemRef, plan };
1976
- for (const pn of portNames) {
1977
- const fieldGoType = portFieldGoType(f.ports[pn], comp.inputPorts, `fanout port '${pn}' of node '${node.id}'`, asBinding, plan);
1978
- inits.push(emitPortInit(pn, f.ports[pn], fieldGoType, resolveRef, comp.inputPorts, plan));
1979
- }
2303
+ const inits = portNames.map((pn) => emitPortInit(pn, f.ports[pn], comp, plan, typedNodes, priorHere, { asBinding, asBase: elemVar }));
1980
2304
  return [`${il}ep_${s} := ${structName}{${inits.join(", ")}}`];
1981
2305
  };
1982
2306
  const lines = [];
@@ -2000,19 +2324,25 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
2000
2324
  // empty id-list => empty connection (handler not called), matching run_behavior.
2001
2325
  lines.push(`${indent}if len(pi_${s}) > 0 {`);
2002
2326
  lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: pi_${s}}`);
2003
- lines.push(`${indent}\tfo_${s}, fo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
2004
- lines.push(`${indent}\tif !fo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${f.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
2005
- lines.push(`${indent}\tif fo_${s}.IsError {`, `${indent}\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under 'fail' policy: "+fo_${s}.Err`)}`, `${indent}\t}`);
2006
- lines.push(`${indent}\tif len(fo_${s}.Rows) != len(pi_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("FANOUT_BATCH_RESULT_MISMATCH", `fmt.Sprintf("fanout '${node.id}': batched handler must return a list aligned to the deduped id list (want %d)", len(pi_${s}))`)}`, `${indent}\t}`);
2327
+ // the fanout element row is a named record → the handler returns the aligned element WIRES ([]WireRow),
2328
+ // and each is de-boxed strictly via decodeWireElem_* (a de-box mismatch fails closed). A DANGLING id is
2329
+ // a nil wire the zero element (empty dedupeKey), which the dangling drop below excludes.
2330
+ const decElemFn = goDecodeElemName(comp.name, node.id);
2331
+ lines.push(`${indent}\tfo_${s}, fo_${s}Err := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
2332
+ lines.push(`${indent}\tif fo_${s}Err != nil {`, `${indent}\t\treturn ${zeroOut}, opFailed(${JSON.stringify(node.id)}, "fail", fo_${s}Err)`, `${indent}\t}`);
2333
+ lines.push(`${indent}\tif len(fo_${s}) != len(pi_${s}) {`, `${indent}\t\treturn ${zeroOut}, ${goErr("FANOUT_BATCH_RESULT_MISMATCH", `fmt.Sprintf("fanout '${node.id}': batched handler must return a list aligned to the deduped id list (want %d)", len(pi_${s}))`)}`, `${indent}\t}`);
2007
2334
  // ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
2008
2335
  // drop dangling (zero dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
2009
2336
  // structural (the elem type already omits it). cursor is always nil (connection wrap).
2010
- lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}.Rows))`);
2011
- lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}.Rows))`);
2012
- lines.push(`${indent}\tfor fi_${s} := range fo_${s}.Rows {`);
2013
- lines.push(`${indent}\t\tbody_${s} := fo_${s}.Rows[fi_${s}]`);
2337
+ lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}))`);
2338
+ lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}))`);
2339
+ lines.push(`${indent}\tfor fi_${s} := range fo_${s} {`);
2014
2340
  lines.push(`${indent}\t\tvar el_${s} ${elemGo}`);
2015
- lines.push(emitRowCopy(`el_${s}`, `body_${s}`, elemRef, plan, indent.length + 2));
2341
+ lines.push(`${indent}\t\tif fo_${s}[fi_${s}] != nil {`);
2342
+ lines.push(`${indent}\t\t\telm_${s}, elm_${s}Err := ${decElemFn}(fo_${s}[fi_${s}])`);
2343
+ lines.push(`${indent}\t\t\tif elm_${s}Err != nil {`, `${indent}\t\t\t\treturn ${zeroOut}, elm_${s}Err`, `${indent}\t\t\t}`);
2344
+ lines.push(emitRowCopy(`el_${s}`, `elm_${s}`, elemRef, plan, indent.length + 3));
2345
+ lines.push(`${indent}\t\t}`);
2016
2346
  lines.push(`${indent}\t\tkey_${s} := el_${s}.${dkGo}`);
2017
2347
  if (f.drop === "dangling") {
2018
2348
  // dangling: a body whose dedupeKey is the zero value (empty string) is dropped.
@@ -2031,73 +2361,17 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
2031
2361
  lines.push(c);
2032
2362
  return lines.join("\n");
2033
2363
  }
2034
- /**
2035
- * goInputHeadRef (#108) resolve an INPUT port head for a native-expr ref (cond `if` / map `when` /
2036
- * branches / an opt-lane port). The port's declared type comes from the shared `inputPortTypeRef` SSoT,
2037
- * so an OPTIONAL port resolves as `{opt:…}` (native `*T`) and the compiler can see its optionality: a
2038
- * null-compare becomes a real presence test and a coalesce a real deref-or-default, while a
2039
- * REQUIRED-scalar slot fed by an opt fails closed. Go values are read directly off the input struct (no
2040
- * ownership ceremony); a type with no native lowering → null (fail-closed).
2041
- */
2364
+ /** #108: resolve an INPUT port head for a native-expr ref (cond `if` / map `when` / branches). A scalar
2365
+ * port `in.<Field>` of the scalar type; an array port WITH elemType `in.<Field>` of `[]ElemT` (so
2366
+ * `len(in.<Field>)` works); anything else null (fail-closed). */
2042
2367
  function goInputHeadRef(head, comp, plan) {
2368
+ // #139: the port's declared type comes from the inputPortTypeRef SSoT, so an OPTIONAL port resolves as
2369
+ // {opt:…} (native *T) and the compiler sees its optionality; a type with no native lowering → null.
2043
2370
  const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
2044
2371
  if (ref === undefined)
2045
2372
  return null;
2046
2373
  return { expr: `in.${goFieldName(head)}`, ref };
2047
2374
  }
2048
- /**
2049
- * optPortCompileG — the OPTIONAL-input port lane (go). The rust twin's mirror: a component may
2050
- * declare an input port `{opt:T}` (PortSchema `required:false`), and the only native representation of
2051
- * "the value is absent" is `*T`, so such a port never lowers through the required-scalar inference. The
2052
- * port expression compiles with the SHARED runtime-free native-expr compiler (native-expr.ts — the same
2053
- * traversal, type-inference and fail-closed discipline rust uses, so the two agree by construction):
2054
- *
2055
- * - `opt($.x)` / `$.x` at port position → `*T` (the leaf receives the absent value as null, exactly
2056
- * as run_behavior's `evalPorts` passes it).
2057
- * - `coalesce(opt($.x), <literal>)` → the native deref-or-default — a native `T`, no boxed Value.
2058
- * - `ne(opt($.x), null)` / `eq(…, null)` → the native presence test (`!= nil` / `== nil`).
2059
- *
2060
- * Returns null when the port expression reads NO optional input (the caller keeps its existing lowering).
2061
- * A port that DOES read one but does not compile — or that compiles to a fallible expression, which this
2062
- * position cannot hoist — FAILS CLOSED loudly rather than degrade to a required type or a zero value.
2063
- */
2064
- function optPortCompileG(node, inputPorts, where, plan) {
2065
- if (plan === undefined)
2066
- return null;
2067
- if (!exprReadsOptionalInput(node, inputPorts))
2068
- return null;
2069
- const resolveHead = (head) => goInputHeadRef(head, { inputPorts }, plan);
2070
- let c;
2071
- try {
2072
- // zeroOut is unreachable here: a port expression that needs an error return is rejected below
2073
- // (stmts.length > 0), so the backend never renders an early-return with it.
2074
- c = new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
2075
- }
2076
- catch (e) {
2077
- if (!(e instanceof GeneratorFailure))
2078
- throw e;
2079
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port but does not lower to a native Go value: ${e.message}`);
2080
- }
2081
- if (c.stmts.length > 0)
2082
- throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (runtime-free): ${where} ${JSON.stringify(node)} reads an OPTIONAL input port through 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.`);
2083
- return c;
2084
- }
2085
- /** exprReadsOptionalInput — does this port expression reference an OPTIONAL (`required:false`) input
2086
- * port? Language-neutral scan for `ref`/`refOpt` heads bound to an optional port. Decides ONLY which
2087
- * lane a port takes; the lane itself then proves the lowering (or fails closed). */
2088
- function exprReadsOptionalInput(node, inputPorts) {
2089
- if (node === null || typeof node !== "object")
2090
- return false;
2091
- if (Array.isArray(node))
2092
- return node.some((el) => exprReadsOptionalInput(el, inputPorts));
2093
- const rec = node;
2094
- const keys = Object.keys(rec);
2095
- if (keys.length === 1 && (keys[0] === "ref" || keys[0] === "refOpt")) {
2096
- const path = rec[keys[0]];
2097
- return Array.isArray(path) && typeof path[0] === "string" && inputPortIsOptional(inputPorts?.[path[0]]);
2098
- }
2099
- return keys.some((k) => exprReadsOptionalInput(rec[k], inputPorts));
2100
- }
2101
2375
  /**
2102
2376
  * emitCondArm — the struct-native exec of a covered cond node (#108). Mirrors run_behavior's cond
2103
2377
  * lower (behavior.ts:425-429): compile `if` to a native bool, materialize the TAKEN branch to the
@@ -2337,19 +2611,19 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
2337
2611
  notOp: (a) => `(!${a})`,
2338
2612
  structLit: (name, inits) => `${name}{${inits.map((i) => `${goFieldName(i.field)}: ${i.expr}`).join(", ")}}`,
2339
2613
  arrLit: (elemTy, items) => `[]${elemTy}{${items.join(", ")}}`,
2614
+ // a projection port of static string literals — bare `[]string{"a", "b"}` (Go string literals are
2615
+ // already allocation-free; no divergence from the general arr path, so no golden churn).
2616
+ staticStrArr: (literals) => ({ expr: `[]string{${literals.map((s) => JSON.stringify(s)).join(", ")}}`, type: "[]string" }),
2340
2617
  // an opt (`*T`) NONE literal is `nil` (the inner type is not needed for the untyped nil in Go).
2341
2618
  optNone: (_innerTy) => "nil",
2342
2619
  // opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
2343
2620
  optIsSome: (expr) => `(${expr} != nil)`,
2344
2621
  optIsNone: (expr) => `(${expr} == nil)`,
2345
- // opt (`*T`) defaulted to a PURE default. Go has no `unwrap_or`, so this is the same immediately-
2346
- // invoked func literal the `ternary` rendering uses (the established shape in this emitter): bind the
2347
- // pointer ONCE, deref it when present, else yield the default. The default sits in the else branch,
2348
- // so it is evaluated only when absent — run_behavior's lazy right side exactly.
2622
+ // opt (`*T`) defaulted to a PURE default an immediately-invoked func literal: bind the pointer ONCE,
2623
+ // deref when present, else yield the default (evaluated only when absent run_behavior's lazy right).
2349
2624
  optUnwrapOr: (optExpr, defaultExpr, innerTy) => `func() ${innerTy} { if v := ${optExpr}; v != nil { return *v }; return ${defaultExpr} }()`,
2350
- // opt defaulted to a FALLIBLE default — a statement if/else, NOT a func literal: the default's
2351
- // hoisted `return zeroOut, err` early returns must leave the ENCLOSING fn, which a func literal
2352
- // would swallow (it would return from the literal). Only the nil branch evaluates the default.
2625
+ // opt defaulted to a FALLIBLE default — a statement if/else (NOT a func literal): the default's hoisted
2626
+ // `return zeroOut, err` early returns must leave the ENCLOSING fn. Only the nil branch evaluates it.
2353
2627
  optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
2354
2628
  `var ${temp} ${ty}`,
2355
2629
  `if ${bindTemp} := ${optExpr}; ${bindTemp} != nil {`,
@@ -2513,13 +2787,15 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
2513
2787
  * mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
2514
2788
  * stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
2515
2789
  * (change #5 — there is no runtime to skew against once the module is runtime-free). */
2516
- function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
2790
+ function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath, needStrconv = false) {
2517
2791
  const sv = ctx.specVersions;
2518
2792
  const imports = [];
2519
2793
  if (needFmt)
2520
2794
  imports.push(`\t"fmt"`);
2521
2795
  if (needMath)
2522
2796
  imports.push(`\t"math"`);
2797
+ if (needStrconv)
2798
+ imports.push(`\t"strconv"`);
2523
2799
  if (needStrings)
2524
2800
  imports.push(`\t"strings"`);
2525
2801
  if (needSync)
@@ -2557,6 +2833,9 @@ function emit(ctx) {
2557
2833
  const handlerIfaces = [];
2558
2834
  const elemCopiers = [];
2559
2835
  const runnerCodes = [];
2836
+ // the generated strict de-box functions (deduped by named type across all covered components).
2837
+ const decodeFns = [];
2838
+ const decodeEmitted = new Set();
2560
2839
  for (const c of native) {
2561
2840
  inStructs.push(emitInStruct(c, plan));
2562
2841
  const typedNodes = new Map();
@@ -2582,9 +2861,13 @@ function emit(ctx) {
2582
2861
  structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes));
2583
2862
  }
2584
2863
  }
2864
+ // fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
2865
+ assertDeBoxableResults(c, typedNodes, plan);
2585
2866
  // CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
2586
2867
  rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
2587
2868
  handlerIfaces.push(emitHandlerInterface(c, typedNodes, plan));
2869
+ // the strict de-box functions for this component's de-box-eligible (named-record) reads.
2870
+ emitNodeDecodeFns(c, typedNodes, plan, decodeEmitted, decodeFns);
2588
2871
  // augmented-element copiers for covered map...into nodes (over fields + concrete into row).
2589
2872
  const mm = emitMapElemMaterializers(c, typedNodes, plan);
2590
2873
  if (mm)
@@ -2618,7 +2901,9 @@ function emit(ctx) {
2618
2901
  // #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
2619
2902
  // math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
2620
2903
  const needMath = goExprUsed.helpers;
2621
- const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath);
2904
+ // `strconv` is imported iff a generated de-box parses a numeric field (int/float range check).
2905
+ const needStrconv = decodeFns.some((f) => f.includes("strconv."));
2906
+ const header = emitMinimalHeader(ctx, false, needSync, needFmt, needMath, needStrconv);
2622
2907
  const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
2623
2908
  // covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
2624
2909
  // children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
@@ -2642,6 +2927,12 @@ function emit(ctx) {
2642
2927
  ? `// CONCRETE per-node handler-result row structs (typed fields = the node's outType; + error signal).\n${rowStructs.filter((r) => r.trim().length > 0).join("\n\n")}`
2643
2928
  : undefined,
2644
2929
  `// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
2930
+ decodeFns.length
2931
+ ? `// strict de-box wire seam — the consumer implements WireRow/WireList over its wire payload\n// (variant classification); the generated decodeWire_* owns strictness (required/optional, present/\n// absent, error assembly, fail-closed). Concrete probe structs — no boxed Value on the seam.\n${emitProbeWireTypes()}`
2932
+ : undefined,
2933
+ decodeFns.length
2934
+ ? `// Generated strict de-box functions (per named record type; structured Error Value on mismatch).\n${decodeFns.join("\n\n")}`
2935
+ : undefined,
2645
2936
  elemCopiers.length
2646
2937
  ? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
2647
2938
  : undefined,
@@ -2687,6 +2978,199 @@ export function goTypedNativeObserve(ir, runtimeImport) {
2687
2978
  // input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
2688
2979
  // (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
2689
2980
  const inDecoders = native.map((c) => emitInDecoder(c, plan)).join("\n\n");
2981
+ // does any covered read de-box a named record — a componentRef record OR a map/fanout element row
2982
+ // (→ the scripted handler returns a WireRow / []WireRow)?
2983
+ const anyDeBox = native.some((c) => {
2984
+ const tn = new Map();
2985
+ for (const n of c.body)
2986
+ if (!isControlGate(n))
2987
+ tn.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan));
2988
+ return c.body.some((n) => {
2989
+ if ("cond" in n)
2990
+ return false;
2991
+ if ("map" in n || "fanout" in n)
2992
+ return mapFanoutElemDeBoxEligible(n, tn, plan);
2993
+ return deBoxEligible(tn.get(n.id));
2994
+ });
2995
+ });
2996
+ // scripted wire adapter (TEST glue): wraps a scripted bc Value as a WireRow/WireList and classifies each
2997
+ // attribute's native type into a DynamoDB-flavored wire tag (S/N/BOOL/L/M/NULL). A real consumer
2998
+ // implements WireRow over its own AttributeValue map; this exists only so the harness drives the
2999
+ // generated de-box from the same scripted vectors run_behavior uses — so actualWireType observed in a
3000
+ // mismatch is the PRODUCER tag (e.g. "N"), not the collapsed native type.
3001
+ const wireAdapter = anyDeBox
3002
+ ? `// ── scripted wire adapter (TEST glue) ──
3003
+ func scriptedWireTag(v ${PKG}.Value) string {
3004
+ switch v.(type) {
3005
+ case nil:
3006
+ return "NULL"
3007
+ case bool:
3008
+ return "BOOL"
3009
+ case int64, float64:
3010
+ return "N"
3011
+ case string:
3012
+ return "S"
3013
+ case []${PKG}.Value:
3014
+ return "L"
3015
+ case *${PKG}.Obj:
3016
+ return "M"
3017
+ default:
3018
+ return "UNKNOWN"
3019
+ }
3020
+ }
3021
+
3022
+ func scriptedWireRaw(v ${PKG}.Value) string {
3023
+ switch t := v.(type) {
3024
+ case nil:
3025
+ return "null"
3026
+ case bool:
3027
+ if t {
3028
+ return "true"
3029
+ }
3030
+ return "false"
3031
+ case int64:
3032
+ return strconv.FormatInt(t, 10)
3033
+ case float64:
3034
+ s, _ := ${PKG}.PyFloatRepr(t)
3035
+ return s
3036
+ case string:
3037
+ return t
3038
+ default:
3039
+ return ${PKG}.EncodeValue(v)
3040
+ }
3041
+ }
3042
+
3043
+ func probeStringVal(v ${PKG}.Value) StringProbe {
3044
+ if v == nil {
3045
+ return StringProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
3046
+ }
3047
+ if s, ok := v.(string); ok {
3048
+ return StringProbe{Kind: probeGot, Got: s, ActualWireType: "S", Raw: s}
3049
+ }
3050
+ return StringProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
3051
+ }
3052
+
3053
+ func probeNumberVal(v ${PKG}.Value) NumberProbe {
3054
+ if v == nil {
3055
+ return NumberProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
3056
+ }
3057
+ switch v.(type) {
3058
+ case int64, float64:
3059
+ return NumberProbe{Kind: probeGot, Got: scriptedWireRaw(v), ActualWireType: "N", Raw: scriptedWireRaw(v)}
3060
+ }
3061
+ return NumberProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
3062
+ }
3063
+
3064
+ func probeBoolVal(v ${PKG}.Value) BoolProbe {
3065
+ if v == nil {
3066
+ return BoolProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
3067
+ }
3068
+ if b, ok := v.(bool); ok {
3069
+ return BoolProbe{Kind: probeGot, Got: b, ActualWireType: "BOOL", Raw: scriptedWireRaw(v)}
3070
+ }
3071
+ return BoolProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
3072
+ }
3073
+
3074
+ func probeRowVal(v ${PKG}.Value) RowProbe {
3075
+ if v == nil {
3076
+ return RowProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
3077
+ }
3078
+ if o, ok := v.(*${PKG}.Obj); ok {
3079
+ return RowProbe{Kind: probeGot, Got: scriptedWireRow{obj: o}, ActualWireType: "M"}
3080
+ }
3081
+ return RowProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
3082
+ }
3083
+
3084
+ func probeListVal(v ${PKG}.Value) ListProbe {
3085
+ if v == nil {
3086
+ return ListProbe{Kind: probeNull, ActualWireType: "NULL", Raw: "null"}
3087
+ }
3088
+ if a, ok := v.([]${PKG}.Value); ok {
3089
+ return ListProbe{Kind: probeGot, Got: scriptedWireList{items: a}, ActualWireType: "L"}
3090
+ }
3091
+ return ListProbe{Kind: probeWrong, ActualWireType: scriptedWireTag(v), Raw: scriptedWireRaw(v)}
3092
+ }
3093
+
3094
+ type scriptedWireRow struct {
3095
+ obj *${PKG}.Obj
3096
+ }
3097
+
3098
+ func newScriptedWireRow(v ${PKG}.Value) WireRow {
3099
+ o, _ := v.(*${PKG}.Obj)
3100
+ return scriptedWireRow{obj: o}
3101
+ }
3102
+
3103
+ func (w scriptedWireRow) field(name string) (${PKG}.Value, bool) {
3104
+ if w.obj == nil {
3105
+ return nil, false
3106
+ }
3107
+ return w.obj.Get(name)
3108
+ }
3109
+
3110
+ func (w scriptedWireRow) Keys() []string {
3111
+ if w.obj == nil {
3112
+ return nil
3113
+ }
3114
+ return w.obj.Keys
3115
+ }
3116
+
3117
+ func (w scriptedWireRow) ProbeString(field string) StringProbe {
3118
+ if v, ok := w.field(field); ok {
3119
+ return probeStringVal(v)
3120
+ }
3121
+ return StringProbe{Kind: probeAbsent}
3122
+ }
3123
+
3124
+ func (w scriptedWireRow) ProbeNumber(field string) NumberProbe {
3125
+ if v, ok := w.field(field); ok {
3126
+ return probeNumberVal(v)
3127
+ }
3128
+ return NumberProbe{Kind: probeAbsent}
3129
+ }
3130
+
3131
+ func (w scriptedWireRow) ProbeBool(field string) BoolProbe {
3132
+ if v, ok := w.field(field); ok {
3133
+ return probeBoolVal(v)
3134
+ }
3135
+ return BoolProbe{Kind: probeAbsent}
3136
+ }
3137
+
3138
+ func (w scriptedWireRow) ProbeRow(field string) RowProbe {
3139
+ if v, ok := w.field(field); ok {
3140
+ return probeRowVal(v)
3141
+ }
3142
+ return RowProbe{Kind: probeAbsent}
3143
+ }
3144
+
3145
+ func (w scriptedWireRow) ProbeList(field string) ListProbe {
3146
+ if v, ok := w.field(field); ok {
3147
+ return probeListVal(v)
3148
+ }
3149
+ return ListProbe{Kind: probeAbsent}
3150
+ }
3151
+
3152
+ type scriptedWireList struct {
3153
+ items []${PKG}.Value
3154
+ }
3155
+
3156
+ func (l scriptedWireList) Len() int { return len(l.items) }
3157
+ func (l scriptedWireList) ElemString(i int) StringProbe { return probeStringVal(l.items[i]) }
3158
+ func (l scriptedWireList) ElemNumber(i int) NumberProbe { return probeNumberVal(l.items[i]) }
3159
+ func (l scriptedWireList) ElemBool(i int) BoolProbe { return probeBoolVal(l.items[i]) }
3160
+ func (l scriptedWireList) ElemRow(i int) RowProbe { return probeRowVal(l.items[i]) }
3161
+ func (l scriptedWireList) ElemList(i int) ListProbe { return probeListVal(l.items[i]) }
3162
+
3163
+ // FailureDetail exposes the structured Error Value fields WITHOUT naming the local ErrorDetail type, so
3164
+ // the harness can assert the de-box's kind / field / expectedType / actualWireType / rawValue (TEST
3165
+ // glue — same package as the covered module, so it may read the fields directly).
3166
+ func (e *BehaviorError) FailureDetail() (kind, model, field, expectedType, actualWireType, rawValue string, ok bool) {
3167
+ if e.Detail == nil {
3168
+ return "", "", "", "", "", "", false
3169
+ }
3170
+ d := e.Detail
3171
+ return d.Kind, d.Model, d.Field, d.ExpectedType, d.ActualWireType, d.RawValue, true
3172
+ }`
3173
+ : "";
2690
3174
  // The observe companion drives EVERY covered component, so its handler type param H must satisfy
2691
3175
  // EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
2692
3176
  const combinedConstraint = `allNativeRawHandlers`;
@@ -2712,35 +3196,56 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
2712
3196
  continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
2713
3197
  const ref = typedNodes.get(n.id);
2714
3198
  if ("fanout" in n) {
2715
- // v3: scripted fanout impl — drain one batched outcome and decode the aligned []Value into the
2716
- // per-body elem rows (a null array element materializes to the zero elem row = dangling).
2717
3199
  const f = n.fanout;
2718
- const elemRowRef = fanoutItemsElemRef(n, ref, plan);
3200
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
2719
3201
  const elemT = rawElemStructName(c.name, n.id);
2720
3202
  const batchT = rawRowStructName(c.name, n.id);
2721
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2722
3203
  const portsT = `${portsStructName(c.name, n.id)}_batch`;
2723
3204
  const foBoundT = boundGoType(n, c, typedNodes, plan);
2724
- methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) (${batchT}, bool) {
3205
+ if (elemDeBox) {
3206
+ // a record-element fanout returns the aligned element WIRES; the covered runner de-boxes each.
3207
+ // A null aligned body is a DANGLING id — a nil wire (the runner maps it to the zero elem row).
3208
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) ([]WireRow, error) {
3209
+ src, ok := s.next(${JSON.stringify(f.component)})
3210
+ if !ok {
3211
+ return nil, unknownComponent(${JSON.stringify(f.component)})
3212
+ }
3213
+ if src.isErr {
3214
+ return nil, leafFailure(src.err)
3215
+ }
3216
+ arr, _ := src.ok.([]${PKG}.Value)
3217
+ wires := make([]WireRow, 0, len(arr))
3218
+ for _, ev := range arr {
3219
+ if ev == nil {
3220
+ wires = append(wires, nil)
3221
+ continue
3222
+ }
3223
+ wires = append(wires, newScriptedWireRow(ev))
3224
+ }
3225
+ return wires, nil
3226
+ }`);
3227
+ continue;
3228
+ }
3229
+ const elemRowRef = fanoutItemsElemRef(n, ref, plan);
3230
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
3231
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${foBoundT}) (${batchT}, error) {
2725
3232
  src, ok := s.next(${JSON.stringify(f.component)})
2726
3233
  if !ok {
2727
- return ${batchT}{}, false
3234
+ return ${batchT}{}, unknownComponent(${JSON.stringify(f.component)})
2728
3235
  }
2729
3236
  if src.isErr {
2730
- return ${batchT}{IsError: true, Err: src.err}, true
3237
+ return ${batchT}{}, leafFailure(src.err)
2731
3238
  }
2732
3239
  arr, _ := src.ok.([]${PKG}.Value)
2733
3240
  rows := make([]${elemT}, 0, len(arr))
2734
3241
  for _, ev := range arr {
2735
- // v3: a null aligned body is a DANGLING id — decode it to the zero elem row (empty dedupeKey
2736
- // field), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
2737
3242
  if ev == nil {
2738
3243
  rows = append(rows, ${elemT}{})
2739
3244
  continue
2740
3245
  }
2741
3246
  rows = append(rows, ${elemDecode}(ev))
2742
3247
  }
2743
- return ${batchT}{Rows: rows}, true
3248
+ return ${batchT}{Rows: rows}, nil
2744
3249
  }`);
2745
3250
  continue;
2746
3251
  }
@@ -2748,72 +3253,172 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
2748
3253
  const m = n.map;
2749
3254
  const batched = m.batched === true;
2750
3255
  const arrRef = ref;
2751
- const elemRowRef = mapElemRowRef(n, arrRef, plan);
3256
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
2752
3257
  const elemT = rawElemStructName(c.name, n.id);
2753
3258
  const batchT = rawRowStructName(c.name, n.id);
2754
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2755
3259
  const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
2756
- const retT = batched ? batchT : elemT;
2757
3260
  const mapBoundT = boundGoType(n, c, typedNodes, plan);
3261
+ if (elemDeBox) {
3262
+ // a record-element map returns the element WIRE(s); the covered runner de-boxes each strictly.
3263
+ if (batched) {
3264
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) ([]WireRow, error) {
3265
+ src, ok := s.next(${JSON.stringify(m.component)})
3266
+ if !ok {
3267
+ return nil, unknownComponent(${JSON.stringify(m.component)})
3268
+ }
3269
+ if src.isErr {
3270
+ return nil, leafFailure(src.err)
3271
+ }
3272
+ arr, _ := src.ok.([]${PKG}.Value)
3273
+ wires := make([]WireRow, 0, len(arr))
3274
+ for _, ev := range arr {
3275
+ wires = append(wires, newScriptedWireRow(ev))
3276
+ }
3277
+ return wires, nil
3278
+ }`);
3279
+ }
3280
+ else {
3281
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (WireRow, error) {
3282
+ src, ok := s.next(${JSON.stringify(m.component)})
3283
+ if !ok {
3284
+ return nil, unknownComponent(${JSON.stringify(m.component)})
3285
+ }
3286
+ if src.isErr {
3287
+ return nil, leafFailure(src.err)
3288
+ }
3289
+ return newScriptedWireRow(src.ok), nil
3290
+ }`);
3291
+ }
3292
+ continue;
3293
+ }
3294
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
3295
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2758
3296
  if (batched) {
2759
- methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${batchT}, bool) {
3297
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${batchT}, error) {
2760
3298
  src, ok := s.next(${JSON.stringify(m.component)})
2761
3299
  if !ok {
2762
- return ${batchT}{}, false
3300
+ return ${batchT}{}, unknownComponent(${JSON.stringify(m.component)})
2763
3301
  }
2764
3302
  if src.isErr {
2765
- return ${batchT}{IsError: true, Err: src.err}, true
3303
+ return ${batchT}{}, leafFailure(src.err)
2766
3304
  }
2767
3305
  arr, _ := src.ok.([]${PKG}.Value)
2768
3306
  rows := make([]${elemT}, 0, len(arr))
2769
3307
  for _, ev := range arr {
2770
3308
  rows = append(rows, ${elemDecode}(ev))
2771
3309
  }
2772
- return ${batchT}{Rows: rows}, true
3310
+ return ${batchT}{Rows: rows}, nil
2773
3311
  }`);
2774
3312
  }
2775
3313
  else {
2776
- methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${elemT}, bool) {
3314
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${elemT}, error) {
2777
3315
  src, ok := s.next(${JSON.stringify(m.component)})
2778
3316
  if !ok {
2779
- return ${elemT}{}, false
3317
+ return ${elemT}{}, unknownComponent(${JSON.stringify(m.component)})
2780
3318
  }
2781
3319
  if src.isErr {
2782
- return ${elemT}{IsError: true, Err: src.err}, true
3320
+ return ${elemT}{}, leafFailure(src.err)
2783
3321
  }
2784
- return ${elemDecode}(src.ok), true
3322
+ return ${elemDecode}(src.ok), nil
2785
3323
  }`);
2786
3324
  }
2787
- void retT;
2788
3325
  continue;
2789
3326
  }
2790
3327
  const node = n;
2791
3328
  const rowT = rawRowStructName(c.name, node.id);
2792
- const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
2793
3329
  const nodeBoundT = boundGoType(node, c, typedNodes, plan);
2794
- // #129 (TEST glue): support the reference scriptedHandlers' echo forms (PROTOCOL.md §3.5) — the
2795
- // adapter normalizes `{"echo":"port","port":"X"}` / `{"echo":"items"}` to the port NAME, and the
2796
- // scripted handler
2797
- // ECHOES a NATIVE port back as its result. This makes a port-plane pin non-vacuous: the node's
2798
- // observed result IS the native value that flowed into its ports struct, so a dropped / re-boxed /
2799
- // zero-defaulted port diverges from run_behavior instead of passing. A port is echoable when its
2800
- // native type IS the node's outType (that is exactly when the row's Val can hold it); `ports` is
2801
- // named only when at least one port qualifies (else the param stays `_` so a non-echo node
2802
- // compiles clean / unused-param-free).
2803
- const echoable = echoablePorts(c, node, ref, plan, typedNodes);
2804
- const portsParam = echoable.length > 0 ? "ports" : "_";
2805
- const echoBranch = echoable
2806
- .map((ep) => `\tif src.echo == ${JSON.stringify(ep)} {\n\t\treturn ${rowT}{Val: ports.${goPortFieldName(ep)}}, true\n\t}\n`)
2807
- .join("");
2808
- methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, bool) {
3330
+ // a de-box-eligible node's scripted handler returns a scriptedWireRow (the scripted Value
3331
+ // wrapped as a WireRow that classifies each attribute's native type into a DynamoDB-flavored wire
3332
+ // tag); the covered module's generated decodeWire_* then probes it. This mirrors a real consumer,
3333
+ // whose handler returns its wire payload for BC to de-box the de-box no longer lives here.
3334
+ if (deBoxEligible(ref)) {
3335
+ // a record-containing top returns its wire (WireRow / []WireRow / nilable WireRow /
3336
+ // map[string]WireRow) the covered runner de-boxes each record. #129: an arr node with an `items`
3337
+ // port echoes the NATIVE port array; it is serialized back to []WireRow so the de-box round-trips.
3338
+ const top = recordTop(ref);
3339
+ const wireT = goWireTypeForTop(top);
3340
+ // echo-into-record (#141 fixtures): a named de-box node whose result WRAPS an input port — the
3341
+ // scripted leaf echoes port P into the record's single field, so the input-port value flows into
3342
+ // the de-boxed result and the runtime non-vacuity #139/#143/#144 assert survives the record wrap.
3343
+ let namedEcho = "";
3344
+ if (top.kind === "named") {
3345
+ const decl = findDecl(plan, top.innerName);
3346
+ const recordGo = renderTypeRef(ref);
3347
+ // a SINGLE-field record admits a scalar-into-field wrap (echo:port + wrap:"v" → {v: ports.X}).
3348
+ const field = decl !== undefined && decl.fields.length === 1 ? decl.fields[0] : undefined;
3349
+ const fieldGo = field !== undefined ? renderTypeRef(field.type) : null;
3350
+ for (const pn of Object.keys(node.ports)) {
3351
+ let portGo = null;
3352
+ try {
3353
+ portGo = portFieldGoType(node.ports[pn], c, plan, typedNodes, `echo port '${pn}'`);
3354
+ }
3355
+ catch {
3356
+ portGo = null;
3357
+ }
3358
+ if (portGo === recordGo) {
3359
+ // case 2: the port IS the whole record (its type == the node's record type) → echo it
3360
+ // DIRECTLY (no field wrap), matching run_behavior's plain echo:port of a whole-record port.
3361
+ namedEcho += `\tif src.echo == ${JSON.stringify(pn)} {\n\t\treturn newScriptedWireRow(${serializeTyped(`ports.${goPortFieldName(pn)}`, ref, plan)}), nil\n\t}\n`;
3362
+ }
3363
+ else if (fieldGo !== null && field !== undefined && portGo === fieldGo) {
3364
+ // case 1: a scalar port wraps into the record's single field ({v: ports.X}).
3365
+ namedEcho += `\tif src.echo == ${JSON.stringify(pn)} {\n\t\treturn newScriptedWireRow(${serializeTyped(`${recordGo}{${goFieldName(field.name)}: ports.${goPortFieldName(pn)}}`, ref, plan)}), nil\n\t}\n`;
3366
+ }
3367
+ }
3368
+ }
3369
+ const canEcho = (top.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items")) || namedEcho !== "";
3370
+ const portsParam = canEcho ? "ports" : "_";
3371
+ let body;
3372
+ if (top.kind === "named") {
3373
+ body = `${namedEcho}\treturn newScriptedWireRow(src.ok), nil`;
3374
+ }
3375
+ else if (top.kind === "arr") {
3376
+ const elemRef = ref.elem;
3377
+ const itemsF = goPortFieldName("items");
3378
+ const echoBranch = canEcho
3379
+ ? `\tif src.echo == "items" {\n\t\twires := make([]WireRow, 0, len(ports.${itemsF}))\n\t\tfor i := range ports.${itemsF} {\n\t\t\twires = append(wires, newScriptedWireRow(${serializeTyped(`ports.${itemsF}[i]`, elemRef, plan)}))\n\t\t}\n\t\treturn wires, nil\n\t}\n`
3380
+ : "";
3381
+ body = `${echoBranch}\tarr, _ := src.ok.([]${PKG}.Value)\n\twires := make([]WireRow, 0, len(arr))\n\tfor _, ev := range arr {\n\t\twires = append(wires, newScriptedWireRow(ev))\n\t}\n\treturn wires, nil`;
3382
+ }
3383
+ else if (top.kind === "opt") {
3384
+ body = `\tif src.ok == nil {\n\t\treturn nil, nil\n\t}\n\treturn newScriptedWireRow(src.ok), nil`;
3385
+ }
3386
+ else {
3387
+ body = `\tobj, _ := src.ok.(*${PKG}.Obj)\n\twires := make(map[string]WireRow)\n\tif obj != nil {\n\t\tfor _, k := range obj.Keys {\n\t\t\twires[k] = newScriptedWireRow(obj.Vals[k])\n\t\t}\n\t}\n\treturn wires, nil`;
3388
+ }
3389
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${wireT}, error) {
2809
3390
  src, ok := s.next(${JSON.stringify(node.component)})
2810
3391
  if !ok {
2811
- return ${rowT}{}, false
3392
+ return nil, unknownComponent(${JSON.stringify(node.component)})
2812
3393
  }
2813
3394
  if src.isErr {
2814
- return ${rowT}{IsError: true, Err: src.err}, true
3395
+ return nil, leafFailure(src.err)
2815
3396
  }
2816
- ${echoBranch} return ${decode}(src.ok), true
3397
+ ${body}
3398
+ }`);
3399
+ continue;
3400
+ }
3401
+ const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
3402
+ // #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
3403
+ // reference scriptedHandlers' `echo === "items"` — the scripted handler ECHOES the NATIVE `items`
3404
+ // port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
3405
+ // observed result is exactly the native array that flowed into its ports struct, so a dropped or
3406
+ // boxed native array would diverge from run_behavior. `ports` is named only when the echo path
3407
+ // uses it (else the param stays `_` so a non-echo node compiles clean / unused-param-free).
3408
+ const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
3409
+ const portsParam = canEcho ? "ports" : "_";
3410
+ const echoBranch = canEcho
3411
+ ? `\tif src.echo == "items" {\n\t\treturn ${rowT}{Val: ports.${goPortFieldName("items")}}, nil\n\t}\n`
3412
+ : "";
3413
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(${portsParam} ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, error) {
3414
+ src, ok := s.next(${JSON.stringify(node.component)})
3415
+ if !ok {
3416
+ return ${rowT}{}, unknownComponent(${JSON.stringify(node.component)})
3417
+ }
3418
+ if src.isErr {
3419
+ return ${rowT}{}, leafFailure(src.err)
3420
+ }
3421
+ ${echoBranch} return ${decode}(src.ok), nil
2817
3422
  }`);
2818
3423
  }
2819
3424
  }
@@ -2856,15 +3461,13 @@ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
2856
3461
  for _, e := range entries {
2857
3462
  eo, _ := e.(*${PKG}.JObj)
2858
3463
  // #129: an \`echo\` directive names an input port to echo back (mirrors the reference
2859
- // scriptedHandlers' PROTOCOL.md §3.5 echo forms). It is drained together with the ok
2860
- // payload. \`{"echo":"port","port":"X"}\` is normalized to the port NAME here, so the
2861
- // generated node method just compares \`src.echo\` against its echoable port names —
2862
- // \`{"echo":"items"}\` is already that shape.
3464
+ // scriptedHandlers' \`echo === "items"\`). It is drained together with the ok payload.
2863
3465
  echo := ""
2864
3466
  if echoNode, has := eo.Get("echo"); has {
2865
3467
  echo, _ = echoNode.(string)
3468
+ // normalize echo:port to the port NAME X so a de-box handler's echo comparison fires;
3469
+ // echo:items already names the port directly.
2866
3470
  if echo == "port" {
2867
- echo = ""
2868
3471
  if portNode, hasPort := eo.Get("port"); hasPort {
2869
3472
  echo, _ = portNode.(string)
2870
3473
  }
@@ -2914,11 +3517,14 @@ func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
2914
3517
  package behaviors
2915
3518
 
2916
3519
  import (
2917
- "sync"
3520
+ ${anyDeBox ? '\t"strconv"\n\n' : ""} "sync"
2918
3521
 
2919
3522
  ${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
2920
3523
  )
2921
3524
 
3525
+ // scripted wire adapter (TEST glue) — wraps a scripted Value as a WireRow the generated de-box probes.
3526
+ ${wireAdapter}
3527
+
2922
3528
  // typed -> Value serializers (TEST glue only — the covered module has none).
2923
3529
  ${serializers}
2924
3530
 
@@ -2952,16 +3558,6 @@ ${arms}
2952
3558
  }
2953
3559
  `;
2954
3560
  }
2955
- /** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
2956
- * declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row's Val. A port
2957
- * with a different type is not echoable (the row could not hold it), and a NAMED outType has no Val at
2958
- * all (its row carries the struct's fields flattened — see emitDecodeRow). */
2959
- function echoablePorts(comp, node, ref, plan, typedNodes) {
2960
- if (ref.kind === "named")
2961
- return [];
2962
- const want = renderTypeRef(ref);
2963
- return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp.inputPorts, `port '${p}'`, undefined, plan, typedNodes) === want);
2964
- }
2965
3561
  /** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
2966
3562
  * that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
2967
3563
  * elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize