behavior-contracts 0.8.5 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,21 +614,14 @@ 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
- /**
53
- * portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free), read off the ONE
54
- * port lowering (compilePortNode / native-expr). The covered ports struct carries NO `dslcontracts.Value`
55
- * field: every port lowers to a concrete Go type, and a port that cannot lower FAILS CLOSED. The field
56
- * type is the compiled expression's TypeRef, so it can never disagree with the initializer (emitPortInit),
57
- * which is the SAME lowering read for its value. `where` names the emission site for the error.
58
- */
59
- function portFieldGoType(node, comp, plan, typedNodes, where = "port", opts) {
60
- const c = compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), where, opts);
61
- return c.renderedType ?? renderTypeRef(c.ref);
617
+ /** scalar TypeRef → PortFieldType (native go scalar kind). */
618
+ function goScalarPortField(scalar) {
619
+ return scalar === "string" ? "string" : scalar === "int" ? "int64" : scalar === "float" ? "float64" : "bool";
62
620
  }
63
621
  /**
64
622
  * goNativeResolveHead — the ONE ref-head resolver the runtime-free native-expr compiler reads, for cond
65
- * `if`/branches, a map/fanout `when` guard, AND a port (go twin Go's value semantics need no ownership
66
- * ceremony). A head is a `$as` element binding, a prior body node's typed local, or an input port.
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.
67
625
  */
68
626
  function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
69
627
  return (head) => {
@@ -78,18 +636,14 @@ function goNativeResolveHead(comp, plan, typedNodes, isPrior, opts) {
78
636
  /**
79
637
  * compilePortNode — lower a port expression to its native ports-struct field via the runtime-free
80
638
  * native-expr compiler (native-expr.ts): the ONE port lowering. The port's field TYPE is the compiled
81
- * `ref`, its initializer the compiled `expr` — so the two are derived from a SINGLE lowering and can
82
- * never disagree. Any closed operator, a literal, a ref (input scalar/array/map, prior-node scalar/array,
83
- * `$as` element field), a concat, a static array, an optional-input read — all resolve here. A port that
84
- * cannot lower FAILS CLOSED loudly. A lowering that is FALLIBLE (hoists statements) is a covered SHAPE
85
- * returned here as-is; only the value site (emitPortInit) rejects it, since a ports-struct literal cannot
86
- * host statements — the coverage gate/type site must still see it as covered.
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.
87
643
  */
88
644
  function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
89
645
  const resolveHead = goNativeResolveHead(comp, plan, typedNodes, isPrior, opts);
90
646
  try {
91
- // zeroOut is unreachable: a fallible port is rejected at emitPortInit (a ports-struct literal hosts no
92
- // statements), so the backend never renders an early-return with it.
93
647
  return new NativeExprCompiler(makeGoExprBackend("", resolveHead, plan)).compilePort(node);
94
648
  }
95
649
  catch (e) {
@@ -98,8 +652,73 @@ function compilePortNode(node, comp, plan, typedNodes, isPrior, where, opts) {
98
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}`);
99
653
  }
100
654
  }
101
- /** the prior-node typed-local TypeRef map for a component (control-gate nodes are typeless; a map node
102
- * carries its produced-array ref). Shared by coverage, the struct surface, and the runner. */
655
+ /**
656
+ * portFieldGoType the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
657
+ * ports struct carries NO `dslcontracts.Value` field: every port lowers to a concrete Go type.
658
+ * - a static string-array (projection) → `[]string` (change #2);
659
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `int64`/`float64`;
660
+ * - a string/bool/scalar-input port → the scalar Go type (inferPortType).
661
+ * A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
662
+ * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
663
+ * `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
664
+ */
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);
668
+ }
669
+ function staticArrElems(node) {
670
+ if (node === null || typeof node !== "object" || Array.isArray(node))
671
+ return null;
672
+ const keys = Object.keys(node);
673
+ if (keys.length !== 1 || keys[0] !== "arr")
674
+ return null;
675
+ const arg = node.arr;
676
+ return Array.isArray(arg) ? arg : null;
677
+ }
678
+ /**
679
+ * isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
680
+ * (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
681
+ * policy, relationKind is single/connection or absent, and EVERY node carries outType + the
682
+ * component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
683
+ * the child-present decision from the real parent result. Ports AND the output are evaluated via
684
+ * the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
685
+ * obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
686
+ * PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
687
+ * emit (see assertTypedCoverage) — never a silent re-box.
688
+ */
689
+ /**
690
+ * numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
691
+ * `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
692
+ * the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
693
+ * static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
694
+ * range check the runtime applies is performed HERE at generation time (integral literal in the
695
+ * safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
696
+ * expression for the literal's Value, or null when the node is not a bare number OR the integral
697
+ * literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
698
+ * semantics — the port is NOT covered natively, so the whole component falls through, never a
699
+ * silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
700
+ */
701
+ const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
702
+ function numLiteralGoExpr(node) {
703
+ if (typeof node !== "number" || !Number.isFinite(node))
704
+ return null;
705
+ if (Number.isInteger(node)) {
706
+ if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
707
+ return null; // out of safe window → not covered (runtime INVALID_LITERAL)
708
+ return `${PKG}.Value(int64(${node}))`;
709
+ }
710
+ // fractional / exponent literal → checked float64 (finite, already guaranteed above).
711
+ return `${PKG}.Value(float64(${node}))`;
712
+ }
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). */
103
722
  function buildTypedNodes(comp, plan) {
104
723
  const typedNodes = new Map();
105
724
  for (const n of comp.body) {
@@ -114,25 +733,26 @@ function buildTypedNodes(comp, plan) {
114
733
  }
115
734
  return typedNodes;
116
735
  }
117
- function staticArrElems(node) {
118
- if (node === null || typeof node !== "object" || Array.isArray(node))
119
- return null;
120
- const keys = Object.keys(node);
121
- if (keys.length !== 1 || keys[0] !== "arr")
122
- return null;
123
- const arg = node.arr;
124
- return Array.isArray(arg) ? arg : null;
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
+ }
125
745
  }
126
- /** Does a port lower to a native value? The ELIGIBILITY gate ASKS the one port lowering (compilePortNode
127
- * / native-expr) whether it can lower this port, rather than re-deriving the shape rules — so the gate
128
- * and the emitter can never disagree. A port that does not lower (or lowers fallibly) fails closed. */
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. */
129
749
  function portIsStatic(node, comp, plan, typedNodes, asBinding) {
130
750
  try {
131
751
  compilePortNode(node, comp, plan, typedNodes, (h) => typedNodes.has(h), "port", asBinding !== undefined ? { asBinding, asBase: asBinding.name } : undefined);
132
752
  return true;
133
753
  }
134
754
  catch {
135
- return false; // an eligibility probe: any lowering failure ⇒ not native-covered here.
755
+ return false;
136
756
  }
137
757
  }
138
758
  /** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
@@ -180,8 +800,8 @@ function isFullyTyped(comp) {
180
800
  }
181
801
  return true;
182
802
  }
183
- function isNativeRaw(comp, plan) {
184
- return isSequentialComponentRefRead(comp, plan) && isFullyTyped(comp);
803
+ function isNativeRaw(comp) {
804
+ return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
185
805
  }
186
806
  /**
187
807
  * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
@@ -196,7 +816,7 @@ function isNativeRaw(comp, plan) {
196
816
  * element struct by copying the over element's typed fields + materializing the into field from the
197
817
  * aligned batch raw — NO boxed Value on any plane.
198
818
  */
199
- function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
819
+ function coveredMapNode(n, bodyIds, comp) {
200
820
  if (n === null || typeof n !== "object" || !("map" in n))
201
821
  return false;
202
822
  const m = n.map;
@@ -213,7 +833,7 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
213
833
  // A map node must carry an outType (its ELEMENT type per the recorder SoT — authoring.ts recordMap
214
834
  // holds `node.outType = mapElem`, the PRE-array-wrap element; the emitter synthesizes the produced
215
835
  // `[]element`). A map WITHOUT outType is uncovered. Legacy hand-built IR that already carries the
216
- // `{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.
217
837
  const outT = n.outType;
218
838
  if (outT === undefined || typeof outT !== "object")
219
839
  return false;
@@ -233,22 +853,14 @@ function coveredMapNode(n, bodyIds, comp, plan, typedNodes) {
233
853
  return false;
234
854
  }
235
855
  const ports = m.ports;
856
+ const plan = planForComp(comp);
857
+ const typedNodes = buildTypedNodes(comp, plan);
236
858
  const asBinding = mapAsBinding(comp, n, typedNodes, plan);
237
859
  for (const p of Object.keys(ports))
238
860
  if (!portIsStatic(ports[p], comp, plan, typedNodes, asBinding))
239
861
  return false;
240
862
  return true;
241
863
  }
242
- /** the `$as` element binding for a covered map/fanout node's element ports (name + over-element TypeRef),
243
- * or undefined when the over element type does not resolve (the node is then uncovered upstream). */
244
- function mapAsBinding(comp, node, typedNodes, plan) {
245
- try {
246
- return { name: node.map.as, ref: mapOverElemInfo(comp, node, typedNodes, plan).elemRef, plan };
247
- }
248
- catch {
249
- return undefined;
250
- }
251
- }
252
864
  /**
253
865
  * coveredFanoutNode (v3) — a FANOUT node (first-class `@refs` id-list fan-out) is covered when:
254
866
  * - over is a static ref to a PRIOR BODY NODE's typed arr field OR an INPUT array port with elemType,
@@ -257,7 +869,7 @@ function mapAsBinding(comp, node, typedNodes, plan) {
257
869
  * The fanout de-boxes to: one deduped batched dispatch → first-seen dedupe (by the elem's dedupeKey
258
870
  * field) + dangling drop + implicitSource strip + connection wrap {items, cursor:nil} — NO boxed Value.
259
871
  */
260
- function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
872
+ function coveredFanoutNode(n, bodyIds, comp) {
261
873
  if (n === null || typeof n !== "object" || !("fanout" in n))
262
874
  return false;
263
875
  const f = n.fanout;
@@ -277,6 +889,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
277
889
  return false;
278
890
  }
279
891
  const ports = f.ports;
892
+ const plan = planForComp(comp);
893
+ const typedNodes = buildTypedNodes(comp, plan);
280
894
  let asBinding;
281
895
  try {
282
896
  asBinding = { name: f.as, ref: fanoutOverElemInfo(comp, n, typedNodes, plan).elemRef, plan };
@@ -294,8 +908,8 @@ function coveredFanoutNode(n, bodyIds, comp, plan, typedNodes) {
294
908
  * reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
295
909
  * NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
296
910
  * (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
297
- function isSequentialComponentRefRead(comp, plan) {
298
- return coverageRejectReason(comp, plan) === null;
911
+ function isSequentialComponentRefRead(comp) {
912
+ return coverageRejectReason(comp) === null;
299
913
  }
300
914
  /**
301
915
  * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
@@ -305,11 +919,12 @@ function isSequentialComponentRefRead(comp, plan) {
305
919
  * never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
306
920
  * user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
307
921
  */
308
- function coverageRejectReason(comp, plan) {
922
+ function coverageRejectReason(comp) {
923
+ const crPlan = planForComp(comp);
924
+ const crTypedNodes = buildTypedNodes(comp, crPlan);
309
925
  const ep = execPlan(comp);
310
926
  if (ep === null)
311
927
  return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
312
- const typedNodes = buildTypedNodes(comp, plan);
313
928
  // A parallel-stage member must be a componentRef node (no map) so the static parallel orchestration
314
929
  // is a clean fan-out — the bounded-concurrency error-precedence protocol is proven for that shape.
315
930
  // A map child inside a parallel stage still falls through (its per-element loop is a further shape).
@@ -354,7 +969,7 @@ function coverageRejectReason(comp, plan) {
354
969
  if ("fanout" in n) {
355
970
  // v3: a first-class fanout node (deduped connection list-in fan-out) IS covered when its over is a
356
971
  // covered ref (prior-node arr / input array with elemType) and its element ports are static.
357
- if (!coveredFanoutNode(n, bodyIds, comp, plan, typedNodes))
972
+ if (!coveredFanoutNode(n, bodyIds, comp))
358
973
  return `node '${n.id}' is a non-covered fanout shape (over an input array without elemType, or a dynamic element port)`;
359
974
  continue;
360
975
  }
@@ -362,7 +977,7 @@ function coverageRejectReason(comp, plan) {
362
977
  // #86 pt2 batched map...into / #93 shape 2/3 / #108 guarded map (no into) / map-over-input (with
363
978
  // elemType) ARE covered. A non-covered map shape (guard+into heterogeneous / over-input without
364
979
  // elemType) falls through to the loud fail-closed reject.
365
- if (!coveredMapNode(n, bodyIds, comp, plan, typedNodes))
980
+ if (!coveredMapNode(n, bodyIds, comp))
366
981
  return `node '${n.id}' is a non-covered map shape (guard+into heterogeneous, or over an input array without elemType)`;
367
982
  continue;
368
983
  }
@@ -393,7 +1008,7 @@ function coverageRejectReason(comp, plan) {
393
1008
  if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
394
1009
  return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
395
1010
  for (const p of Object.keys(cr.ports))
396
- if (portIsStatic(cr.ports[p], comp, plan, typedNodes) === false)
1011
+ if (portIsStatic(cr.ports[p], comp, crPlan, crTypedNodes) === false)
397
1012
  return `node '${n.id}' port '${p}' is not statically resolvable`;
398
1013
  }
399
1014
  if (!outputIsNativeLowerable(comp))
@@ -401,20 +1016,25 @@ function coverageRejectReason(comp, plan) {
401
1016
  return null;
402
1017
  }
403
1018
  /** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
404
- function assertTypedCoverage(comp, plan) {
405
- if (!isSequentialComponentRefRead(comp, plan))
1019
+ function assertTypedCoverage(comp) {
1020
+ if (!isSequentialComponentRefRead(comp))
406
1021
  return;
407
1022
  if (isFullyTyped(comp))
408
1023
  return;
409
1024
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#77): component '${comp.name}' is a sequential componentRef read but lacks outType/outputType on every node — the combined emitter de-boxes the RESULT into the node's outType struct, so a covered read MUST be fully typed (bc#45). Add outType annotations, or regenerate on go-straightline-native (boxed result).`);
410
1025
  }
411
1026
  // ── native ports struct + PortReader ──────────────────────────────────────────────────
412
- function emitPortsStruct(comp, node, plan, typedNodes, asBinding) {
1027
+ function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
413
1028
  const structName = portsStructName(comp.name, node.id);
414
1029
  const portNames = Object.keys(node.ports);
415
1030
  if (portNames.length === 0) {
416
1031
  return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
417
1032
  }
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
+ }
418
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));
419
1039
  const fieldNames = portNames.map(goPortFieldName);
420
1040
  const fields = portNames
@@ -434,7 +1054,7 @@ ${fields}
434
1054
  //
435
1055
  // The covered path no longer crosses a generic `RawValue`/`RawRow` on the RESULT plane. Each
436
1056
  // covered node has a CONCRETE row struct `RawRow_<comp>_<node>` whose fields are the node's
437
- // 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,
438
1058
  // per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
439
1059
  // the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
440
1060
  // each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
@@ -442,7 +1062,7 @@ ${fields}
442
1062
  //
443
1063
  // A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
444
1064
  // named-struct node has one Go field per outType field (same names/types as the outType decl), so
445
- // `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
446
1066
  // mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
447
1067
  /** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
448
1068
  * prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
@@ -474,7 +1094,7 @@ function nodeMethodName(compName, nodeId) {
474
1094
  * emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
475
1095
  * covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
476
1096
  * Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
477
- * 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.
478
1098
  */
479
1099
  function emitRawRowStructs(comp, typedNodes, plan) {
480
1100
  const parts = [];
@@ -489,7 +1109,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
489
1109
  const elemRowRef = fanoutItemsElemRef(n, ref, plan);
490
1110
  parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
491
1111
  parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for fanout '${n.id}': the aligned per-body rows.\n` +
492
- `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}`);
493
1113
  continue;
494
1114
  }
495
1115
  if ("map" in n) {
@@ -502,7 +1122,7 @@ function emitRawRowStructs(comp, typedNodes, plan) {
502
1122
  const elemRowRef = mapElemRowRef(n, arrRef, plan);
503
1123
  parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
504
1124
  parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
505
- `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}`);
506
1126
  continue;
507
1127
  }
508
1128
  parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
@@ -510,13 +1130,11 @@ function emitRawRowStructs(comp, typedNodes, plan) {
510
1130
  return parts.join("\n\n");
511
1131
  }
512
1132
  /** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
513
- * always carry IsError/Err. */
1133
+ * A row models a SUCCESS; a failure is the second (error) return, so no in-band error signal. */
514
1134
  function emitOneRowStruct(name, ref, plan) {
515
1135
  const lines = [];
516
- 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).`);
517
1137
  lines.push(`type ${name} struct {`);
518
- lines.push(`\tIsError bool`);
519
- lines.push(`\tErr string`);
520
1138
  if (ref.kind === "named") {
521
1139
  const decl = findDecl(plan, ref.name);
522
1140
  for (const f of decl.fields) {
@@ -591,21 +1209,32 @@ function emitHandlerInterface(comp, typedNodes, plan) {
591
1209
  const bt = boundGoType(n, comp, typedNodes, plan);
592
1210
  if ("fanout" in n) {
593
1211
  // fanout (v3): one batched dispatch of the deduped id list. Ports = the batch struct; returns the
594
- // 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.
595
1214
  const portsT = `${portsStructName(comp.name, n.id)}_batch`;
596
- const retT = rawRowStructName(comp.name, n.id);
597
- 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)`);
598
1218
  continue;
599
1219
  }
600
1220
  if ("map" in n) {
601
1221
  const m = n.map;
602
1222
  const batched = m.batched === true;
603
1223
  const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
604
- const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
605
- 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)`);
606
1229
  continue;
607
1230
  }
608
- 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)`);
609
1238
  }
610
1239
  lines.push(`}`);
611
1240
  return lines.join("\n");
@@ -629,7 +1258,7 @@ function emitMapPortsStructs(comp, node, typedNodes, plan) {
629
1258
  // struct element). A pure-static/concat port ignores the binding.
630
1259
  const { elemRef } = mapOverElemInfo(comp, node, typedNodes, plan);
631
1260
  const asBinding = { name: m.as, ref: elemRef, plan };
632
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, plan, typedNodes, asBinding);
1261
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports }, asBinding, plan, typedNodes);
633
1262
  const batchStruct = `${structName}_batch`;
634
1263
  return `${elemStruct}
635
1264
 
@@ -647,7 +1276,7 @@ function emitFanoutPortsStructs(comp, node, typedNodes, plan) {
647
1276
  const structName = portsStructName(comp.name, node.id);
648
1277
  const { elemRef } = fanoutOverElemInfo(comp, node, typedNodes, plan);
649
1278
  const asBinding = { name: f.as, ref: elemRef, plan };
650
- const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, plan, typedNodes, asBinding);
1279
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: f.component, ports: f.ports }, asBinding, plan, typedNodes);
651
1280
  const batchStruct = `${structName}_batch`;
652
1281
  return `${elemStruct}
653
1282
 
@@ -693,18 +1322,16 @@ function fanoutOverElemInfo(comp, node, typedNodes, plan) {
693
1322
  }
694
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)}).`);
695
1324
  }
696
- /** mapNodeArrRef — the map node's PRODUCED-ARRAY TypeRef from its `outType`, normalizing the two IR
697
- * conventions to the one array shape every downstream site assumes (`typedNodes.get(mapId)` is an arr):
698
- * - the RECORDER SoT (authoring.ts recordMap): `node.outType` is the ELEMENT (pre-array-wrap) wrap
699
- * to `{arr:element}` (the map produces `[]element`), matching type-gate's outputType wrap
700
- * (`"map" in n ? {arr:ot} : ot`). This is the declarative-authoring form (graphddb batchWrite/forEach).
701
- * - legacy hand-built IR: `node.outType` already the `{arr:element}` produced form take it as-is.
702
- * A map ELEMENT is never itself an `{arr:…}` in the covered corpus, so the discriminator is unambiguous.
703
- * 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))`). */
704
1332
  function mapNodeArrRef(node, plan) {
705
1333
  const outT = node.outType;
706
- const isArr = outT !== null && typeof outT === "object" && "arr" in outT;
707
- return deriveTypeRef((isArr ? outT : { arr: outT }), plan);
1334
+ return deriveTypeRef({ arr: outT }, plan);
708
1335
  }
709
1336
  /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
710
1337
  * type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
@@ -847,31 +1474,17 @@ function reconstructG(e) {
847
1474
  function inStructName(compName) {
848
1475
  return `In_${sanitize(compName)}`;
849
1476
  }
850
- /**
851
- * inputPortGoType — the native Go type for an input port's declared type. Scalars lower to the concrete
852
- * scalar; `array`/`map` with a declared elemType to `[]ElemT` / `map[string]ElemT`; an OPTIONAL port
853
- * (`{opt:T}` → `required:false`) to `*T` — the native representation of "the value is absent" (nil).
854
- * A declared type with no native lowering stays the boxed Value.
855
- *
856
- * An OPTIONAL port whose inner type has no native lowering FAILS CLOSED: a boxed `Value` cannot carry
857
- * the presence distinction on the covered plane, and emitting the bare inner type would silently turn
858
- * "absent" into a zero value. A port whose optionality cannot be represented is never silently coerced.
859
- */
1477
+ /** Go type for an input port's declared type (scalar → concrete; array+elemType → []ElemT; else Value). */
860
1478
  function inputPortGoType(schema, plan, where = "input port") {
861
1479
  const ref = inputPortTypeRef(schema, plan);
862
1480
  if (ref !== undefined)
863
1481
  return renderTypeRef(ref);
864
1482
  if (inputPortIsOptional(schema))
865
- 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.`);
866
1484
  return `${PKG}.Value`;
867
1485
  }
868
- /** The scalar kind a REQUIRED input port declares, or undefined when it is not a required native scalar.
869
- * An OPTIONAL port is deliberately NOT a scalar kind here: its native type is `*T`, and the callers of
870
- * this function build REQUIRED scalar slots (`in.<Field>` read straight into a non-opt port field).
871
- * 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. */
872
1487
  function inputScalarKind(schema) {
873
- if (inputPortIsOptional(schema))
874
- return undefined;
875
1488
  switch (schema?.type) {
876
1489
  case "string":
877
1490
  case "literal": // a `"literal"` union is a constrained string (see inputPortGoType).
@@ -896,6 +1509,8 @@ function emitInStruct(comp, plan) {
896
1509
  return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
897
1510
  }
898
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).
899
1514
  const types = ports.map((p) => inputPortGoType(comp.inputPorts[p], plan, `input port '${p}' of component '${comp.name}'`));
900
1515
  const nameW = Math.max(0, ...names.map((n) => n.length));
901
1516
  const typeW = Math.max(0, ...types.map((t) => t.length));
@@ -937,15 +1552,6 @@ function emitInDecoder(comp, plan) {
937
1552
  const scalar = inputScalarKind(schema);
938
1553
  const et = schema.elemType;
939
1554
  lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
940
- if (inputPortIsOptional(schema)) {
941
- // An OPTIONAL port decodes into its native *T. materializeExpr's opt de-box IS the wire rule:
942
- // a nil Value (null) → nil (absent), any other value → a pointer to the materialized inner. A key
943
- // that never appears leaves the struct's zero value (nil) — the same absent value. Checked FIRST:
944
- // optionality wraps every inner type (an optional array is *[]T, not []T).
945
- lines.push(`\t\tin.${field} = ${goTypedInternals.materializeExpr("v", inputPortTypeRef(schema, plan), plan)}`);
946
- lines.push(`\t}`);
947
- continue;
948
- }
949
1555
  if ((schema?.type === "array" || schema?.type === "arr") && et !== undefined) {
950
1556
  // #108: decode the boxed Value array into the native []ElemT (test glue — materialize each element
951
1557
  // via the typed materializer; a non-array or non-conforming element is a fail-closed decode error).
@@ -976,11 +1582,9 @@ function emitInDecoder(comp, plan) {
976
1582
  lines.push(`\t\tin.${field} = v`);
977
1583
  }
978
1584
  else {
979
- lines.push(`\t\tt, tok := v.(${scalar})`);
980
- lines.push(`\t\tif !tok {`);
981
- lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected ${scalar}, got "+${PKG}.TypeName(v))`);
982
- lines.push(`\t\t}`);
983
- 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)}`);
984
1588
  }
985
1589
  lines.push(`\t}`);
986
1590
  }
@@ -988,17 +1592,25 @@ function emitInDecoder(comp, plan) {
988
1592
  lines.push(`}`);
989
1593
  return lines.join("\n");
990
1594
  }
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). */
991
1599
  /**
992
- * emitPortInit — the port field initializer, read off the ONE port lowering (compilePortNode / native-
993
- * expr). The field's declared TYPE (portFieldGoType) is the SAME lowering's TypeRef, so type and value
994
- * can never disagree. `isPrior` tests whether a ref head names a settled prior node at this position;
995
- * `opts.asBinding`/`opts.asBase` carry a map/fanout element binding. A FALLIBLE lowering (hoisted
996
- * statements) is rejected HERE a ports-struct literal cannot host statements.
1600
+ * emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
1601
+ * runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
1602
+ * struct field NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
1603
+ * - a static string array (projection) `[]string{"a", "b", ...}` (change #2);
1604
+ * - a bare int/float number literal (e.g. Query `limit: 20`) `int64(20)` / `float64(...)`;
1605
+ * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
1606
+ * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
1607
+ * plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
1608
+ * struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
997
1609
  */
998
1610
  function emitPortInit(pn, expr, comp, plan, typedNodes, isPrior, opts) {
999
1611
  const c = compilePortNode(expr, comp, plan, typedNodes, isPrior, `port '${pn}'`, opts);
1000
1612
  if (c.stmts.length > 0)
1001
- 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. (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.`);
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.`);
1002
1614
  return `${goPortFieldName(pn)}: ${c.expr}`;
1003
1615
  }
1004
1616
  /**
@@ -1059,9 +1671,17 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1059
1671
  const node = comp.body[i];
1060
1672
  const s = sanitize(node.id);
1061
1673
  const structName = portsStructName(comp.name, comp.body[i].id);
1062
- const rowT = rawRowStructName(comp.name, comp.body[i].id);
1063
- lines.push(`\t\tvar row_${s} ${rowT}`);
1064
- 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`);
1065
1685
  lines.push(`\t\trun_${s} := false`);
1066
1686
  lines.push(`\t\tvar ps_${s} ${structName}`);
1067
1687
  lines.push(`\t\t_ = ps_${s}`);
@@ -1133,7 +1753,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1133
1753
  lines.push(`\t\t\t\tdefer wg.Done()`);
1134
1754
  lines.push(`\t\t\t\tdefer func() { <-sem }()`);
1135
1755
  const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
1136
- 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})`);
1137
1758
  lines.push(`\t\t\t}()`);
1138
1759
  lines.push(`\t\t}`);
1139
1760
  }
@@ -1145,20 +1766,53 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1145
1766
  const s = sanitize(node.id);
1146
1767
  const ref = typedNodes.get(node.id);
1147
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.
1148
1774
  lines.push(`\t\tif run_${s} {`);
1149
- 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}`);
1150
1775
  if (meta.policy === "continue") {
1151
- lines.push(`\t\t\tif !${oc}.IsError {`);
1152
- lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 4));
1153
- 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
+ }
1154
1793
  lines.push(`\t\t\t}`);
1155
1794
  }
1156
1795
  else {
1157
- const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1158
- lines.push(`\t\t\tif ${oc}.IsError {`);
1159
- 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)`);
1160
1798
  lines.push(`\t\t\t}`);
1161
- 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
+ }
1162
1816
  lines.push(`\t\t\tproduced_${s} = true`);
1163
1817
  }
1164
1818
  lines.push(`\t\t}`);
@@ -1318,22 +1972,75 @@ function emitNativeRawRunner(comp, plan, flags) {
1318
1972
  }
1319
1973
  lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
1320
1974
  const oc = `row_${s}`;
1321
- lines.push(`${indent}${oc}, ${oc}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
1322
- lines.push(`${indent}if !${oc}Resolved {`, `${indent}\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `${indent}}`);
1323
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;
1324
1984
  if (meta.policy === "continue") {
1325
- lines.push(`${indent}if !${oc}.IsError {`);
1326
- lines.push(copy(indent + "\t"));
1327
- lines.push(`${indent}\tproduced_${s} = true`);
1328
- 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
+ }
1329
2009
  }
1330
2010
  else {
1331
- const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1332
- lines.push(`${indent}if ${oc}.IsError {`);
1333
- lines.push(`${indent}\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
1334
- lines.push(`${indent}}`);
1335
- lines.push(copy(indent));
1336
- 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
+ }
1337
2044
  }
1338
2045
  for (const c of closers)
1339
2046
  lines.push(c);
@@ -1408,7 +2115,8 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1408
2115
  if (!hasGuard)
1409
2116
  return null;
1410
2117
  const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere, { asBinding: { name: asName, ref: overElemRef, plan }, asBase: elemVar });
1411
- return new NativeExprCompiler(makeGoExprBackend(zeroOut, resolveHead, plan)).compileBool(m.when);
2118
+ const be = makeGoExprBackend(zeroOut, resolveHead, plan);
2119
+ return new NativeExprCompiler(be).compileBool(m.when);
1412
2120
  };
1413
2121
  // emit the per-element native ports struct build (shared; `il` is the indent inside the element
1414
2122
  // loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
@@ -1431,6 +2139,12 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1431
2139
  lines.push(`${indent}over_${s} := ${overExpr}`);
1432
2140
  lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
1433
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) : "";
1434
2148
  // emit the per-element guard keep-gate (#108). `il` = indent, `elemVar` = the over element loop var.
1435
2149
  // On skip: batched → `continue` (element excluded from items + keptOver); non-batched → `continue`.
1436
2150
  const emitGuard = (il, elemVar) => {
@@ -1447,7 +2161,7 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1447
2161
  };
1448
2162
  if (batched) {
1449
2163
  // BATCHED (#86 pt2 into / #93 shape 3 no-into / #108 guarded no-into): build kept-element ports,
1450
- // 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).
1451
2165
  // With a guard, `items` (and, for into, `keptOver`) hold only kept elements — matching run_behavior's
1452
2166
  // keptIdx-aligned batch. `into` is never combined with a guard (rejected). no-into collects each row.
1453
2167
  lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
@@ -1465,22 +2179,39 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1465
2179
  // empty items => empty result (handler not called), matching run_behavior.
1466
2180
  lines.push(`${indent}if len(items_${s}) > 0 {`);
1467
2181
  lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
1468
- lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
1469
- lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1470
- 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}`);
1471
- 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}`);
1472
2189
  if (hasInto) {
1473
2190
  // #86 pt2: zip-attach the into field onto each kept over element (augmented element copier).
1474
2191
  lines.push(`${indent}\tfor mk_${s} := range keptOver_${s} {`);
1475
- 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
+ }
1476
2200
  lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1477
2201
  lines.push(`${indent}\t}`);
1478
2202
  }
1479
2203
  else {
1480
2204
  // #93 shape 3 / #108: collect each element DIRECTLY from the aligned per-element row (field copy).
1481
- lines.push(`${indent}\tfor mk_${s} := range mo_${s}.Rows {`);
2205
+ lines.push(`${indent}\tfor mk_${s} := 0; mk_${s} < ${batchLen}; mk_${s}++ {`);
1482
2206
  lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1483
- 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
+ }
1484
2215
  lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1485
2216
  lines.push(`${indent}\t}`);
1486
2217
  }
@@ -1499,15 +2230,30 @@ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, f
1499
2230
  // per-element dispatch (ONE physical request per element). run_behavior passes the over element
1500
2231
  // as the bound value; the element data is fully conveyed by the native ports struct here, so bound
1501
2232
  // stays nil. Byte-equivalence holds because the bound value only feeds handler-internal logic.
1502
- lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ep_${s}, nil)`);
1503
- lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1504
- 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
+ }
1505
2251
  if (hasInto) {
1506
- lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, mo_${s})`);
2252
+ lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, ${elemVar})`);
1507
2253
  }
1508
2254
  else {
1509
2255
  lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1510
- lines.push(emitRowCopy(`el_${s}`, `mo_${s}`, elemRowRef, plan, indent.length + 1));
2256
+ lines.push(emitRowCopy(`el_${s}`, elemVar, elemRowRef, plan, indent.length + 1));
1511
2257
  }
1512
2258
  lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1513
2259
  lines.push(`${indent}}`);
@@ -1578,19 +2324,25 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
1578
2324
  // empty id-list => empty connection (handler not called), matching run_behavior.
1579
2325
  lines.push(`${indent}if len(pi_${s}) > 0 {`);
1580
2326
  lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: pi_${s}}`);
1581
- lines.push(`${indent}\tfo_${s}, fo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
1582
- lines.push(`${indent}\tif !fo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${f.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1583
- 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}`);
1584
- 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}`);
1585
2334
  // ── THE ONE dedupe/drop (mirrors fanoutDedupDrop verbatim): first-seen dedupe by dedupeKey field;
1586
2335
  // drop dangling (zero dedupeKey ≡ interpreter's null/absent-key body); implicitSource strip is
1587
2336
  // structural (the elem type already omits it). cursor is always nil (connection wrap).
1588
- lines.push(`${indent}\tseen_${s} := make(map[string]struct{}, len(fo_${s}.Rows))`);
1589
- lines.push(`${indent}\titems_${s} = make([]${elemGo}, 0, len(fo_${s}.Rows))`);
1590
- lines.push(`${indent}\tfor fi_${s} := range fo_${s}.Rows {`);
1591
- 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} {`);
1592
2340
  lines.push(`${indent}\t\tvar el_${s} ${elemGo}`);
1593
- 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}`);
1594
2346
  lines.push(`${indent}\t\tkey_${s} := el_${s}.${dkGo}`);
1595
2347
  if (f.drop === "dangling") {
1596
2348
  // dangling: a body whose dedupeKey is the zero value (empty string) is dropped.
@@ -1609,15 +2361,12 @@ function emitFanoutArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut
1609
2361
  lines.push(c);
1610
2362
  return lines.join("\n");
1611
2363
  }
1612
- /**
1613
- * goInputHeadRef (#108) resolve an INPUT port head for a native-expr ref (cond `if` / map `when` /
1614
- * branches / an opt-lane port). The port's declared type comes from the shared `inputPortTypeRef` SSoT,
1615
- * so an OPTIONAL port resolves as `{opt:…}` (native `*T`) and the compiler can see its optionality: a
1616
- * null-compare becomes a real presence test and a coalesce a real deref-or-default, while a
1617
- * REQUIRED-scalar slot fed by an opt fails closed. Go values are read directly off the input struct (no
1618
- * ownership ceremony); a type with no native lowering → null (fail-closed).
1619
- */
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). */
1620
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.
1621
2370
  const ref = inputPortTypeRef(comp.inputPorts?.[head], plan);
1622
2371
  if (ref === undefined)
1623
2372
  return null;
@@ -1635,7 +2384,11 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut)
1635
2384
  const c = node.cond;
1636
2385
  const s = sanitize(node.id);
1637
2386
  const priorHere = (head) => priorNodeAt(head, atPos);
1638
- const resolveHead = goNativeResolveHead(comp, plan, typedNodes, priorHere);
2387
+ const resolveHead = (head) => {
2388
+ if (priorHere(head))
2389
+ return { expr: typedLocal(head), ref: typedNodes.get(head) };
2390
+ return goInputHeadRef(head, comp, plan);
2391
+ };
1639
2392
  const be = makeGoExprBackend(zeroOut, resolveHead, plan);
1640
2393
  const compiler = new NativeExprCompiler(be);
1641
2394
  const cond = compiler.compileBool(c.if);
@@ -1866,14 +2619,11 @@ function makeGoExprBackend(zeroOut, resolveHead, plan) {
1866
2619
  // opt (`*T`) null-presence test: present = `(expr != nil)`, absent = `(expr == nil)`.
1867
2620
  optIsSome: (expr) => `(${expr} != nil)`,
1868
2621
  optIsNone: (expr) => `(${expr} == nil)`,
1869
- // opt (`*T`) defaulted to a PURE default. Go has no `unwrap_or`, so this is the same immediately-
1870
- // invoked func literal the `ternary` rendering uses (the established shape in this emitter): bind the
1871
- // pointer ONCE, deref it when present, else yield the default. The default sits in the else branch,
1872
- // 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).
1873
2624
  optUnwrapOr: (optExpr, defaultExpr, innerTy) => `func() ${innerTy} { if v := ${optExpr}; v != nil { return *v }; return ${defaultExpr} }()`,
1874
- // opt defaulted to a FALLIBLE default — a statement if/else, NOT a func literal: the default's
1875
- // hoisted `return zeroOut, err` early returns must leave the ENCLOSING fn, which a func literal
1876
- // 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.
1877
2627
  optCoalesceGuard: (temp, ty, optExpr, bindTemp, dStmts, dExpr) => [
1878
2628
  `var ${temp} ${ty}`,
1879
2629
  `if ${bindTemp} := ${optExpr}; ${bindTemp} != nil {`,
@@ -2037,13 +2787,15 @@ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name
2037
2787
  * mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
2038
2788
  * stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
2039
2789
  * (change #5 — there is no runtime to skew against once the module is runtime-free). */
2040
- function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath) {
2790
+ function emitMinimalHeader(ctx, needStrings, needSync, needFmt, needMath, needStrconv = false) {
2041
2791
  const sv = ctx.specVersions;
2042
2792
  const imports = [];
2043
2793
  if (needFmt)
2044
2794
  imports.push(`\t"fmt"`);
2045
2795
  if (needMath)
2046
2796
  imports.push(`\t"math"`);
2797
+ if (needStrconv)
2798
+ imports.push(`\t"strconv"`);
2047
2799
  if (needStrings)
2048
2800
  imports.push(`\t"strings"`);
2049
2801
  if (needSync)
@@ -2063,14 +2815,12 @@ var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "express
2063
2815
  }
2064
2816
  // ── module assembly ──────────────────────────────────────────────────────────────────
2065
2817
  function emit(ctx) {
2066
- // The type plan is the SSoT the coverage gate + the port lowering both read (the gate ASKS the port
2067
- // lowering whether a port lowers), so it is built up-front — before the coverage checks.
2068
- const plan = buildTypePlan(ctx.ir);
2069
2818
  // Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
2070
2819
  for (const c of ctx.ir.components)
2071
- assertTypedCoverage(c, plan);
2072
- const native = ctx.ir.components.filter((c) => isNativeRaw(c, plan));
2073
- const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c, plan));
2820
+ assertTypedCoverage(c);
2821
+ const native = ctx.ir.components.filter(isNativeRaw);
2822
+ const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
2823
+ const plan = buildTypePlan(ctx.ir);
2074
2824
  const decls = emitTypeDecls(plan);
2075
2825
  // #108: reset the native-expr helper-usage accumulator (set by the cond/guard compiler when it emits
2076
2826
  // a fallible checked-arith helper call — determines whether the helper library + `math` import ship).
@@ -2083,9 +2833,16 @@ function emit(ctx) {
2083
2833
  const handlerIfaces = [];
2084
2834
  const elemCopiers = [];
2085
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();
2086
2839
  for (const c of native) {
2087
2840
  inStructs.push(emitInStruct(c, plan));
2088
- const typedNodes = buildTypedNodes(c, plan);
2841
+ const typedNodes = new Map();
2842
+ // build the full typed-node map FIRST so a map's over-element resolution can see every node.
2843
+ for (const n of c.body)
2844
+ if (!isControlGate(n))
2845
+ typedNodes.set(n.id, "map" in n ? mapNodeArrRef(n, plan) : deriveTypeRef(n.outType, plan)); // #125: control-gate typeless; map は produced-array ref に正規化
2089
2846
  for (const n of c.body) {
2090
2847
  if ("fanout" in n) {
2091
2848
  // v3: a fanout node emits a per-id element ports struct + a batch struct (like a batched map).
@@ -2101,12 +2858,16 @@ function emit(ctx) {
2101
2858
  }
2102
2859
  else {
2103
2860
  // #129: pass typedNodes so a leaf array port fed by a prior node's arr outType types as []ElemT.
2104
- structs.push(emitPortsStruct(c, n, plan, typedNodes));
2861
+ structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes));
2105
2862
  }
2106
2863
  }
2864
+ // fail closed for a record inside a non-named top (never a silent trust) — before emitting anything.
2865
+ assertDeBoxableResults(c, typedNodes, plan);
2107
2866
  // CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
2108
2867
  rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
2109
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);
2110
2871
  // augmented-element copiers for covered map...into nodes (over fields + concrete into row).
2111
2872
  const mm = emitMapElemMaterializers(c, typedNodes, plan);
2112
2873
  if (mm)
@@ -2130,7 +2891,7 @@ function emit(ctx) {
2130
2891
  // minimal, runtime-free header.
2131
2892
  if (nonNative.length > 0) {
2132
2893
  const rejects = nonNative
2133
- .map((c) => ` - component '${c.name}': ${coverageRejectReason(c, plan) ?? "not a covered native read"}`)
2894
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
2134
2895
  .join("\n");
2135
2896
  throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go typed-native (native codegen) surface: ${nonNative.length} component(s) are NOT a covered native read:\n${rejects}\n\n` +
2136
2897
  `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
@@ -2140,7 +2901,9 @@ function emit(ctx) {
2140
2901
  // #108: `math` is imported iff the native-expr compiler emitted a fallible helper (bcExpr_modF uses
2141
2902
  // math.Mod). The helper library itself is baked into the module only when used (goExprUsed.helpers).
2142
2903
  const needMath = goExprUsed.helpers;
2143
- 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);
2144
2907
  const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
2145
2908
  // covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
2146
2909
  // children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
@@ -2164,6 +2927,12 @@ function emit(ctx) {
2164
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")}`
2165
2928
  : undefined,
2166
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,
2167
2936
  elemCopiers.length
2168
2937
  ? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
2169
2938
  : undefined,
@@ -2188,8 +2957,8 @@ function emit(ctx) {
2188
2957
  */
2189
2958
  export function goTypedNativeObserve(ir, runtimeImport) {
2190
2959
  const components = ir.components;
2960
+ const native = components.filter(isNativeRaw);
2191
2961
  const plan = buildTypePlan(ir);
2192
- const native = components.filter((c) => isNativeRaw(c, plan));
2193
2962
  const serializers = emitSerializers(plan);
2194
2963
  const marshallers = goTypedInternals.emitMarshallers(plan);
2195
2964
  const mustHelpers = emitMustHelpers(plan);
@@ -2209,6 +2978,199 @@ export function goTypedNativeObserve(ir, runtimeImport) {
2209
2978
  // input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
2210
2979
  // (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
2211
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
+ : "";
2212
3174
  // The observe companion drives EVERY covered component, so its handler type param H must satisfy
2213
3175
  // EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
2214
3176
  const combinedConstraint = `allNativeRawHandlers`;
@@ -2234,35 +3196,56 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
2234
3196
  continue; // #108: cond has no handler method (pure Expression) — no scripted impl.
2235
3197
  const ref = typedNodes.get(n.id);
2236
3198
  if ("fanout" in n) {
2237
- // v3: scripted fanout impl — drain one batched outcome and decode the aligned []Value into the
2238
- // per-body elem rows (a null array element materializes to the zero elem row = dangling).
2239
3199
  const f = n.fanout;
2240
- const elemRowRef = fanoutItemsElemRef(n, ref, plan);
3200
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
2241
3201
  const elemT = rawElemStructName(c.name, n.id);
2242
3202
  const batchT = rawRowStructName(c.name, n.id);
2243
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2244
3203
  const portsT = `${portsStructName(c.name, n.id)}_batch`;
2245
3204
  const foBoundT = boundGoType(n, c, typedNodes, plan);
2246
- 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) {
2247
3232
  src, ok := s.next(${JSON.stringify(f.component)})
2248
3233
  if !ok {
2249
- return ${batchT}{}, false
3234
+ return ${batchT}{}, unknownComponent(${JSON.stringify(f.component)})
2250
3235
  }
2251
3236
  if src.isErr {
2252
- return ${batchT}{IsError: true, Err: src.err}, true
3237
+ return ${batchT}{}, leafFailure(src.err)
2253
3238
  }
2254
3239
  arr, _ := src.ok.([]${PKG}.Value)
2255
3240
  rows := make([]${elemT}, 0, len(arr))
2256
3241
  for _, ev := range arr {
2257
- // v3: a null aligned body is a DANGLING id — decode it to the zero elem row (empty dedupeKey
2258
- // field), so the runner's dangling-drop excludes it (byte-equal to fanoutDedupDrop).
2259
3242
  if ev == nil {
2260
3243
  rows = append(rows, ${elemT}{})
2261
3244
  continue
2262
3245
  }
2263
3246
  rows = append(rows, ${elemDecode}(ev))
2264
3247
  }
2265
- return ${batchT}{Rows: rows}, true
3248
+ return ${batchT}{Rows: rows}, nil
2266
3249
  }`);
2267
3250
  continue;
2268
3251
  }
@@ -2270,77 +3253,172 @@ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
2270
3253
  const m = n.map;
2271
3254
  const batched = m.batched === true;
2272
3255
  const arrRef = ref;
2273
- const elemRowRef = mapElemRowRef(n, arrRef, plan);
3256
+ const elemDeBox = mapFanoutElemDeBoxEligible(n, typedNodes, plan);
2274
3257
  const elemT = rawElemStructName(c.name, n.id);
2275
3258
  const batchT = rawRowStructName(c.name, n.id);
2276
- const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2277
3259
  const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
2278
- const retT = batched ? batchT : elemT;
2279
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");
2280
3296
  if (batched) {
2281
- 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) {
2282
3298
  src, ok := s.next(${JSON.stringify(m.component)})
2283
3299
  if !ok {
2284
- return ${batchT}{}, false
3300
+ return ${batchT}{}, unknownComponent(${JSON.stringify(m.component)})
2285
3301
  }
2286
3302
  if src.isErr {
2287
- return ${batchT}{IsError: true, Err: src.err}, true
3303
+ return ${batchT}{}, leafFailure(src.err)
2288
3304
  }
2289
3305
  arr, _ := src.ok.([]${PKG}.Value)
2290
3306
  rows := make([]${elemT}, 0, len(arr))
2291
3307
  for _, ev := range arr {
2292
3308
  rows = append(rows, ${elemDecode}(ev))
2293
3309
  }
2294
- return ${batchT}{Rows: rows}, true
3310
+ return ${batchT}{Rows: rows}, nil
2295
3311
  }`);
2296
3312
  }
2297
3313
  else {
2298
- 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) {
2299
3315
  src, ok := s.next(${JSON.stringify(m.component)})
2300
3316
  if !ok {
2301
- return ${elemT}{}, false
3317
+ return ${elemT}{}, unknownComponent(${JSON.stringify(m.component)})
2302
3318
  }
2303
3319
  if src.isErr {
2304
- return ${elemT}{IsError: true, Err: src.err}, true
3320
+ return ${elemT}{}, leafFailure(src.err)
2305
3321
  }
2306
- return ${elemDecode}(src.ok), true
3322
+ return ${elemDecode}(src.ok), nil
2307
3323
  }`);
2308
3324
  }
2309
- void retT;
2310
3325
  continue;
2311
3326
  }
2312
3327
  const node = n;
2313
3328
  const rowT = rawRowStructName(c.name, node.id);
2314
- const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
2315
3329
  const nodeBoundT = boundGoType(node, c, typedNodes, plan);
2316
- // #129 (TEST glue): support the reference scriptedHandlers' echo forms (PROTOCOL.md §3.5) — the
2317
- // adapter normalizes `{"echo":"port","port":"X"}` / `{"echo":"items"}` to the port NAME, and the
2318
- // scripted handler
2319
- // ECHOES a NATIVE port back as its result. This makes a port-plane pin non-vacuous: the node's
2320
- // observed result IS the native value that flowed into its ports struct, so a dropped / re-boxed /
2321
- // zero-defaulted port diverges from run_behavior instead of passing. A port is echoable when its
2322
- // native type IS the node's outType (that is exactly when the row's Val can hold it); `ports` is
2323
- // named only when at least one port qualifies (else the param stays `_` so a non-echo node
2324
- // compiles clean / unused-param-free).
2325
- const echoable = echoablePorts(c, node, ref, plan, typedNodes);
2326
- const portsParam = echoable.length > 0 ? "ports" : "_";
2327
- // a NAMED outType row is FLATTENED (no Val) echo a named port by copying the struct's fields;
2328
- // a scalar/arr/opt outType row holds the port in its single Val field.
2329
- const echoRow = (ep) => ref.kind === "named"
2330
- ? `${rowT}{${findDecl(plan, ref.name).fields.map((f) => `${goFieldName(f.name)}: ports.${goPortFieldName(ep)}.${goFieldName(f.name)}`).join(", ")}}`
2331
- : `${rowT}{Val: ports.${goPortFieldName(ep)}}`;
2332
- const echoBranch = echoable
2333
- .map((ep) => `\tif src.echo == ${JSON.stringify(ep)} {\n\t\treturn ${echoRow(ep)}, true\n\t}\n`)
2334
- .join("");
2335
- 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) {
2336
3390
  src, ok := s.next(${JSON.stringify(node.component)})
2337
3391
  if !ok {
2338
- return ${rowT}{}, false
3392
+ return nil, unknownComponent(${JSON.stringify(node.component)})
2339
3393
  }
2340
3394
  if src.isErr {
2341
- return ${rowT}{IsError: true, Err: src.err}, true
3395
+ return nil, leafFailure(src.err)
2342
3396
  }
2343
- ${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
2344
3422
  }`);
2345
3423
  }
2346
3424
  }
@@ -2383,15 +3461,13 @@ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
2383
3461
  for _, e := range entries {
2384
3462
  eo, _ := e.(*${PKG}.JObj)
2385
3463
  // #129: an \`echo\` directive names an input port to echo back (mirrors the reference
2386
- // scriptedHandlers' PROTOCOL.md §3.5 echo forms). It is drained together with the ok
2387
- // payload. \`{"echo":"port","port":"X"}\` is normalized to the port NAME here, so the
2388
- // generated node method just compares \`src.echo\` against its echoable port names —
2389
- // \`{"echo":"items"}\` is already that shape.
3464
+ // scriptedHandlers' \`echo === "items"\`). It is drained together with the ok payload.
2390
3465
  echo := ""
2391
3466
  if echoNode, has := eo.Get("echo"); has {
2392
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.
2393
3470
  if echo == "port" {
2394
- echo = ""
2395
3471
  if portNode, hasPort := eo.Get("port"); hasPort {
2396
3472
  echo, _ = portNode.(string)
2397
3473
  }
@@ -2441,11 +3517,14 @@ func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
2441
3517
  package behaviors
2442
3518
 
2443
3519
  import (
2444
- "sync"
3520
+ ${anyDeBox ? '\t"strconv"\n\n' : ""} "sync"
2445
3521
 
2446
3522
  ${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
2447
3523
  )
2448
3524
 
3525
+ // scripted wire adapter (TEST glue) — wraps a scripted Value as a WireRow the generated de-box probes.
3526
+ ${wireAdapter}
3527
+
2449
3528
  // typed -> Value serializers (TEST glue only — the covered module has none).
2450
3529
  ${serializers}
2451
3530
 
@@ -2479,14 +3558,6 @@ ${arms}
2479
3558
  }
2480
3559
  `;
2481
3560
  }
2482
- /** echoablePorts (TEST glue) — the node's ports whose NATIVE type is exactly the node's outType, in
2483
- * declaration order. Those are the ports a scripted `echo: "<port>"` can return as the row (a scalar/arr/
2484
- * opt row via its Val; a NAMED row via its flattened fields copied from the struct port). A port with a
2485
- * different type is not echoable (the row could not hold it). */
2486
- function echoablePorts(comp, node, ref, plan, typedNodes) {
2487
- const want = renderTypeRef(ref);
2488
- return Object.keys(node.ports).filter((p) => portFieldGoType(node.ports[p], comp, plan, typedNodes, `port '${p}'`) === want);
2489
- }
2490
3561
  /** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
2491
3562
  * that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
2492
3563
  * elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize