behavior-contracts 0.3.0 → 0.5.0

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.
Files changed (51) hide show
  1. package/README.md +78 -29
  2. package/dist/generator/core.d.ts +14 -0
  3. package/dist/generator/core.d.ts.map +1 -1
  4. package/dist/generator/core.js +1 -0
  5. package/dist/generator/core.js.map +1 -1
  6. package/dist/generator/emit-raw-abi-go.d.ts.map +1 -1
  7. package/dist/generator/emit-raw-abi-go.js +2 -1
  8. package/dist/generator/emit-raw-abi-go.js.map +1 -1
  9. package/dist/generator/emit-straightline-go.d.ts +2 -0
  10. package/dist/generator/emit-straightline-go.d.ts.map +1 -1
  11. package/dist/generator/emit-straightline-go.js +37 -19
  12. package/dist/generator/emit-straightline-go.js.map +1 -1
  13. package/dist/generator/emit-straightline-native-go.d.ts +54 -0
  14. package/dist/generator/emit-straightline-native-go.d.ts.map +1 -0
  15. package/dist/generator/emit-straightline-native-go.js +835 -0
  16. package/dist/generator/emit-straightline-native-go.js.map +1 -0
  17. package/dist/generator/emit-straightline-native-rust.d.ts +27 -0
  18. package/dist/generator/emit-straightline-native-rust.d.ts.map +1 -0
  19. package/dist/generator/emit-straightline-native-rust.js +624 -0
  20. package/dist/generator/emit-straightline-native-rust.js.map +1 -0
  21. package/dist/generator/emit-straightline-rust.d.ts.map +1 -1
  22. package/dist/generator/emit-straightline-rust.js +5 -12
  23. package/dist/generator/emit-straightline-rust.js.map +1 -1
  24. package/dist/generator/emit-straightline-typed-go.d.ts +17 -0
  25. package/dist/generator/emit-straightline-typed-go.d.ts.map +1 -1
  26. package/dist/generator/emit-straightline-typed-go.js +4 -1
  27. package/dist/generator/emit-straightline-typed-go.js.map +1 -1
  28. package/dist/generator/emit-straightline-typed-rust.d.ts +16 -0
  29. package/dist/generator/emit-straightline-typed-rust.d.ts.map +1 -1
  30. package/dist/generator/emit-straightline-typed-rust.js +2 -0
  31. package/dist/generator/emit-straightline-typed-rust.js.map +1 -1
  32. package/dist/generator/emit-straightline-typescript.d.ts.map +1 -1
  33. package/dist/generator/emit-straightline-typescript.js +4 -10
  34. package/dist/generator/emit-straightline-typescript.js.map +1 -1
  35. package/dist/generator/emit-typed-native-go.d.ts +63 -0
  36. package/dist/generator/emit-typed-native-go.d.ts.map +1 -0
  37. package/dist/generator/emit-typed-native-go.js +1840 -0
  38. package/dist/generator/emit-typed-native-go.js.map +1 -0
  39. package/dist/generator/emit-typed-native-rust.d.ts +52 -0
  40. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -0
  41. package/dist/generator/emit-typed-native-rust.js +1673 -0
  42. package/dist/generator/emit-typed-native-rust.js.map +1 -0
  43. package/dist/generator/index.d.ts +2 -0
  44. package/dist/generator/index.d.ts.map +1 -1
  45. package/dist/generator/index.js +16 -0
  46. package/dist/generator/index.js.map +1 -1
  47. package/dist/generator/straightline.d.ts +46 -0
  48. package/dist/generator/straightline.d.ts.map +1 -1
  49. package/dist/generator/straightline.js +65 -0
  50. package/dist/generator/straightline.js.map +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,1840 @@
1
+ import { GeneratorFailure } from "./core.js";
2
+ import { goStraightlineInternals } from "./emit-straightline-go.js";
3
+ import { goTypedInternals } from "./emit-straightline-typed-go.js";
4
+ import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
5
+ import { concurrencyPlan, execPlan, analyzeSequentialOps, classifyExpr, buildComponentPlan, } from "./straightline.js";
6
+ const PKG = "dslcontracts";
7
+ /** goErr — build the LOCAL concrete error value for a behavior/plan failure on the covered read
8
+ * plane (runtime-free). The covered module declares `type BehaviorError struct { Code, Message string }`
9
+ * (see emitBehaviorErrorType); a `*BehaviorError` satisfies the runner's `error` return. This REPLACES
10
+ * the old `dslcontracts.NewBehaviorFailure`/`NewPlanFailure` (the last error-plane runtime coupling) —
11
+ * the SAME codes are preserved verbatim so the observed failure is byte-equal to run_behavior. */
12
+ function goErr(code, msgExpr) {
13
+ return `&BehaviorError{Code: ${JSON.stringify(code)}, Message: ${msgExpr}}`;
14
+ }
15
+ /** emitBehaviorErrorType — the LOCAL concrete error type baked into the covered module so a runner can
16
+ * fail closed with ZERO bc-runtime import. `*BehaviorError` satisfies `error`. */
17
+ function emitBehaviorErrorType() {
18
+ return `// BehaviorError — the LOCAL concrete failure type for the covered read plane (runtime-free): a
19
+ // covered runner returns *BehaviorError (which satisfies error) instead of a bc-runtime failure, so
20
+ // the fully-covered module imports ZERO bc runtime. Codes match run_behavior verbatim (byte-equal).
21
+ type BehaviorError struct {
22
+ Code string
23
+ Message string
24
+ }
25
+
26
+ func (e *BehaviorError) Error() string { return e.Code + ": " + e.Message }
27
+
28
+ // FailureCode exposes the stable failure code (byte-equal to run_behavior) WITHOUT a bc-runtime type:
29
+ // a caller that wants the code type-asserts the small local interface { FailureCode() string } rather
30
+ // than a bc failure type — keeping this module runtime-free while a harness can still read the code.
31
+ func (e *BehaviorError) FailureCode() string { return e.Code }`;
32
+ }
33
+ const { sanitize, emitExpr } = goStraightlineInternals;
34
+ const { goFieldName, renderTypeRef, goScalar, serializeTyped, emitTypeDecls, emitSerializers, emitTypedValue, emitMustHelpers, } = goTypedInternals;
35
+ /** typed node-result local — MUST match goTypedInternals.typedLocal (`t_<id>`) so the reused
36
+ * emitTypedValue (output assembly via struct field access) reads the same locals. */
37
+ function typedLocal(nodeId) {
38
+ return `t_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
39
+ }
40
+ function goPortFieldName(wire) {
41
+ const safe = wire.replace(/[^A-Za-z0-9_]/g, "_");
42
+ const head = safe.charAt(0);
43
+ return /[a-z]/.test(head) ? head.toUpperCase() + safe.slice(1) : "F_" + safe;
44
+ }
45
+ function portsStructName(compName, nodeId) {
46
+ // EXPORTED so a consumer (and the test bench, both in a DIFFERENT Go package) can name the ports
47
+ // type to implement the per-component Handler_<comp> interface's concrete Node_* method signatures.
48
+ return `PortsNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
49
+ }
50
+ function inferPortType(node, inputPorts) {
51
+ if (staticArrElems(node) !== null)
52
+ return "Value";
53
+ const e = classifyExpr(node);
54
+ switch (e.kind) {
55
+ case "str":
56
+ case "concat":
57
+ return "string";
58
+ case "bool":
59
+ return "bool";
60
+ case "ref": {
61
+ if (e.path.length === 1) {
62
+ const s = inputPorts[e.path[0]];
63
+ if (s) {
64
+ if (s.type === "string")
65
+ return "string";
66
+ if (s.type === "int")
67
+ return "int64";
68
+ if (s.type === "float")
69
+ return "float64";
70
+ if (s.type === "bool")
71
+ return "bool";
72
+ }
73
+ }
74
+ return "Value";
75
+ }
76
+ default:
77
+ return "Value";
78
+ }
79
+ }
80
+ /** A static string-array port (every element a static string literal) — the graphddb `projection`
81
+ * shape. Returns the list of literal strings, or null when the node is not that shape. */
82
+ function staticStringArrElems(node) {
83
+ const arr = staticArrElems(node);
84
+ if (arr === null)
85
+ return null;
86
+ const out = [];
87
+ for (const el of arr) {
88
+ const e = classifyExpr(el);
89
+ if (e.kind !== "str")
90
+ return null;
91
+ out.push(e.value);
92
+ }
93
+ return out;
94
+ }
95
+ /**
96
+ * portFieldGoType — the NATIVE Go type for a covered port field (bc#90 / runtime-free). The covered
97
+ * ports struct carries NO `dslcontracts.Value` field: every port lowers to a concrete Go type.
98
+ * - a static string-array (projection) → `[]string` (change #2);
99
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `int64`/`float64`;
100
+ * - a string/bool/scalar-input port → the scalar Go type (inferPortType).
101
+ * A port that would otherwise infer to the boxed `Value` (a dynamic operator, a non-static array, a
102
+ * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no
103
+ * `dslcontracts.Value` fallback on the native module). `where` names the emission site for the error.
104
+ */
105
+ function portFieldGoType(node, inputPorts, where = "port") {
106
+ if (staticStringArrElems(node) !== null)
107
+ return "[]string";
108
+ const num = numLiteralGoExpr(node);
109
+ if (num !== null)
110
+ return Number.isInteger(node) ? "int64" : "float64";
111
+ const ft = inferPortType(node, inputPorts);
112
+ if (ft === "Value") {
113
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): ${where} '${JSON.stringify(node)}' does not lower to a native Go type (would need a boxed dslcontracts.Value). The covered read plane is runtime-free and admits NO boxed escape — the covered corpus ports are string / bool / static-string-array (projection) / bare-number (limit) only. Regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
114
+ }
115
+ return ft;
116
+ }
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;
125
+ }
126
+ /**
127
+ * isNativeRawComponent — a component covered by the combined read emitter. Strictly sequential
128
+ * (no real concurrency), body is ALL componentRef (no map/cond), default 'fail'/'continue'
129
+ * policy, relationKind is single/connection or absent, and EVERY node carries outType + the
130
+ * component carries outputType. relationKind:single / bindField ARE the point — inline exec makes
131
+ * the child-present decision from the real parent result. Ports AND the output are evaluated via
132
+ * the primitive SSoT over a Value-space portScope (so a dynamic operator in a port or an
133
+ * obj-assembly output is fine — the SSoT handles fail-closed semantics; only the RESULT plane +
134
+ * PORT container go native). A covered SHAPE that lacks outType is a fail-closed error raised at
135
+ * emit (see assertTypedCoverage) — never a silent re-box.
136
+ */
137
+ /**
138
+ * numLiteralGoExpr — a BARE numeric port literal (JSON number, e.g. the `limit: 20` Query port).
139
+ * `classifyExpr` deliberately leaves a bare number `dynamic` (its checked int/float range rules are
140
+ * the evaluate SSoT, expr.go evalNumberLiteral), so a plain number never reaches the str/bool/null
141
+ * static branch. But a number literal IS statically resolvable to a native Go Value: the SAME
142
+ * range check the runtime applies is performed HERE at generation time (integral literal in the
143
+ * safe 2^53-1 window → int64; a fractional/exponent literal → the checked float64). Returns the Go
144
+ * expression for the literal's Value, or null when the node is not a bare number OR the integral
145
+ * literal is out of the safe window (that out-of-range case keeps the runtime INVALID_LITERAL
146
+ * semantics — the port is NOT covered natively, so the whole component falls through, never a
147
+ * silently-wrong inline). Mirrors coderuntime.evalNumberLiteral exactly.
148
+ */
149
+ const NUM_SAFE_INT = 9007199254740991; // 2^53 - 1 (expr.go safeInt)
150
+ function numLiteralGoExpr(node) {
151
+ if (typeof node !== "number" || !Number.isFinite(node))
152
+ return null;
153
+ if (Number.isInteger(node)) {
154
+ if (node < -NUM_SAFE_INT || node > NUM_SAFE_INT)
155
+ return null; // out of safe window → not covered (runtime INVALID_LITERAL)
156
+ return `${PKG}.Value(int64(${node}))`;
157
+ }
158
+ // fractional / exponent literal → checked float64 (finite, already guaranteed above).
159
+ return `${PKG}.Value(float64(${node}))`;
160
+ }
161
+ /** A port node is static (literal / ref / concat of those / static arr / bare number literal) —
162
+ * resolvable to a native Go Value with no NewObj / no evaluate interpreter. */
163
+ function portIsStatic(node) {
164
+ const arr = staticArrElems(node);
165
+ if (arr !== null)
166
+ return arr.every(portIsStatic);
167
+ if (numLiteralGoExpr(node) !== null)
168
+ return true; // bare number literal (e.g. Query `limit`)
169
+ const e = classifyExpr(node);
170
+ switch (e.kind) {
171
+ case "str":
172
+ case "bool":
173
+ case "null":
174
+ case "ref":
175
+ return true;
176
+ case "concat":
177
+ return e.parts.every((p) => portIsStatic(reconstructG(p)));
178
+ default:
179
+ return false;
180
+ }
181
+ }
182
+ /** The output must lower to a typed struct/value with no Value-scope read: a ref whose head is a
183
+ * BODY NODE (typed), an obj/arr of native-lowerable outputs, or a concat of them. A ref to an
184
+ * input param or a non-concat operator is NOT native-lowerable here (falls through). */
185
+ function outputIsNativeLowerable(comp) {
186
+ const bodyIds = new Set(comp.body.map((n) => n.id));
187
+ const walk = (node) => {
188
+ const arr = staticArrElems(node);
189
+ if (arr !== null)
190
+ return arr.every(walk);
191
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
192
+ const keys = Object.keys(node);
193
+ if (keys.length === 1 && keys[0] === "obj") {
194
+ const obj = node.obj;
195
+ return Object.keys(obj).every((k) => walk(obj[k]));
196
+ }
197
+ if (keys.length === 1 && keys[0] === "arr")
198
+ return true; // handled by staticArrElems above
199
+ }
200
+ const e = classifyExpr(node);
201
+ if (e.kind === "str" || e.kind === "bool" || e.kind === "null")
202
+ return true;
203
+ // ref OR refOpt to a body node (typed). refOpt is how a produced-aware output reads a skippable
204
+ // relationSingle child: the head node's produced flag gates it, yielding opt-nil when unproduced.
205
+ if (e.kind === "ref")
206
+ return bodyIds.has(e.path[0]); // must ref a body node (typed), not input
207
+ if (e.kind === "concat")
208
+ return e.parts.every((p) => walk(reconstructG(p)));
209
+ return false;
210
+ };
211
+ return walk(comp.output);
212
+ }
213
+ /** A native-raw component MUST be fully typed (outType on every node + outputType). */
214
+ function isFullyTyped(comp) {
215
+ if (comp.outputType === undefined)
216
+ return false;
217
+ for (const n of comp.body) {
218
+ if (n.outType === undefined)
219
+ return false;
220
+ }
221
+ return true;
222
+ }
223
+ function isNativeRaw(comp) {
224
+ return isSequentialComponentRefRead(comp) && isFullyTyped(comp);
225
+ }
226
+ /**
227
+ * coveredMapNode — the #86 part 2 covered map shape (nestedBatchGet). A BATCHED `map` node whose:
228
+ * - over is a static ref (to a prior node's typed collection field OR an input array),
229
+ * - `into` is set (zip-attach the batched handler result onto every over element),
230
+ * - every element port is static (literal / ref into the element binding / concat / static arr),
231
+ * - element outType is a NAMED struct (the augmented element type — over element + the into field),
232
+ * - no `when` guard (guard+into => heterogeneous elements, not one []T — stays boxed),
233
+ * - default 'fail' policy, relationKind single/connection or absent.
234
+ * This is the struct-native de-box of a typed `map...into` element collection: build the per-element
235
+ * native ports struct, dispatch the batched handler ONCE (RawValue out), and assemble each augmented
236
+ * element struct by copying the over element's typed fields + materializing the into field from the
237
+ * aligned batch raw — NO boxed Value on any plane.
238
+ */
239
+ function coveredMapNode(n, bodyIds) {
240
+ if (n === null || typeof n !== "object" || !("map" in n))
241
+ return false;
242
+ const m = n.map;
243
+ // #86 pt2: batched map...into. #93 shape 2: NON-batched map...into (per-element dispatch). #93
244
+ // shape 3: batched map with NO into (connection-collect / plain list). A `when` guard + into is
245
+ // still uncovered (heterogeneous elements — not one []T). A `when` guard WITHOUT into would be
246
+ // covered in principle but the covered corpus does not exercise it; keep it out to stay conservative.
247
+ if (m.when !== undefined)
248
+ return false;
249
+ if (m.policy !== undefined && m.policy !== "fail")
250
+ return false;
251
+ if (m.relationKind !== undefined && m.relationKind !== "single" && m.relationKind !== "connection")
252
+ return false;
253
+ // `over` must be a ref to a PRIOR BODY NODE's typed arr field (the de-box resolves the over element
254
+ // type from the parent node's typed struct — mapOverElemType). A map over an INPUT-param array is
255
+ // not covered here (its element type comes from inputPorts, not a typed node struct) — it falls
256
+ // through to the boxed typed / straight-line path.
257
+ const overE = classifyExpr(m.over);
258
+ if (overE.kind !== "ref" || !bodyIds.has(overE.path[0]))
259
+ return false;
260
+ const ports = m.ports;
261
+ for (const p of Object.keys(ports))
262
+ if (!portIsStatic(ports[p]))
263
+ return false;
264
+ return true;
265
+ }
266
+ /** A covered read SHAPE (typing-independent) — sequential componentRef reads + covered map...into,
267
+ * OR a real-concurrency staged plan (bc#87) whose parallel-stage members are plain componentRef point
268
+ * reads. A real-concurrency plan whose parallel members include a map / bindField-relation child is
269
+ * NOT covered here (falls through) — the static-parallel orchestration covers the componentRef fan-out
270
+ * (the GroupAccessRead shape); a parallel map/relation-child stage is a further shape (out of scope). */
271
+ function isSequentialComponentRefRead(comp) {
272
+ return coverageRejectReason(comp) === null;
273
+ }
274
+ /**
275
+ * coverageRejectReason — the DIAGNOSTIC twin of isSequentialComponentRefRead (bc#86/#89). Returns
276
+ * null when the component IS a covered native read; otherwise a SPECIFIC human-readable reason naming
277
+ * the exact disqualifier and the offending node id. The predicate logic is IDENTICAL to
278
+ * isSequentialComponentRefRead (which is defined as `coverageRejectReason(c) === null`) so the two can
279
+ * never disagree. This is the message the fail-closed dispatch surfaces per non-native component — the
280
+ * user's signal to add native coverage, fix a genuinely-dynamic spec, or use the --ir dynamic surface.
281
+ */
282
+ function coverageRejectReason(comp) {
283
+ const ep = execPlan(comp);
284
+ if (ep === null)
285
+ return "component is not a straight-line/staged sequential exec plan (no static exec plan)";
286
+ // A parallel-stage member must be a plain componentRef point read (no map, no bindField) so the
287
+ // static parallel orchestration is a clean fan-out — the bounded-concurrency error-precedence
288
+ // protocol is proven for that shape. A map / bindField child inside a parallel stage falls through.
289
+ for (const [i] of ep.parallelStageOf) {
290
+ const n = comp.body[i];
291
+ if (!("component" in n))
292
+ return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
293
+ if (n.bindField !== undefined)
294
+ return `node '${n.id}' is a bindField relation child inside a parallel stage (not native-covered)`;
295
+ }
296
+ const bodyIds = new Set(comp.body.map((b) => b.id));
297
+ for (const n of comp.body) {
298
+ if ("cond" in n)
299
+ return `node '${n.id}' is a cond branch (not yet native-covered)`;
300
+ if ("map" in n) {
301
+ // #86 pt2 batched map...into / #93 shape 2 non-batched map...into / #93 shape 3 batched map NO
302
+ // into ARE covered (over a prior body node's typed arr); other map shapes fall through.
303
+ if (!coveredMapNode(n, bodyIds))
304
+ return `node '${n.id}' is a non-covered map shape (guarded/over-input)`;
305
+ continue;
306
+ }
307
+ if (!("component" in n))
308
+ return `node '${n.id}' is not a componentRef, map, or cond node (not native-covered)`;
309
+ const cr = n;
310
+ // Only default 'fail' policy on covered reads: 'continue' (and any skip source, incl. bindField)
311
+ // makes a node UNPRODUCED at runtime, whose output ref must yield the unproduced rep — a
312
+ // produced-aware output lowering the struct-only path does not do yet (documented follow-up).
313
+ // Such components fall through to the boxed typed / straight-line path (correct, just not
314
+ // struct-de-plumbed). relationKind:single WITHOUT bindField (the #74/#323 core) IS covered.
315
+ if (cr.policy !== undefined && cr.policy !== "fail")
316
+ return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
317
+ // #86 part 1: a relationKind:single child WITH bindField (relationSingle) IS covered. When the
318
+ // bound parent field is null/absent the child is UNPRODUCED — the produced-aware output lowering
319
+ // (emitProducedAwareValue) then yields the unproduced rep (null / opt-nil) for a ref/refOpt into
320
+ // it, matching run_behavior's writeUnproduced. #93 shape 1: a bindField on a relationKind:connection
321
+ // child is ALSO covered — the connection's unproduced rep is the empty connection {items:[],cursor:null}
322
+ // (unproducedValue("connection")), which emitProducedAwareValue emits explicitly for a skipped
323
+ // connection node (a ref into it yields {items:[],cursor:null}, matching run_behavior).
324
+ if (cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
325
+ return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
326
+ if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
327
+ return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
328
+ for (const p of Object.keys(cr.ports))
329
+ if (portIsStatic(cr.ports[p]) === false)
330
+ return `node '${n.id}' port '${p}' is not statically resolvable`;
331
+ }
332
+ if (!outputIsNativeLowerable(comp))
333
+ return "output is not natively lowerable";
334
+ return null;
335
+ }
336
+ /** Fail closed if a covered read SHAPE lacks the typed annotations (no silent boxed re-box). */
337
+ function assertTypedCoverage(comp) {
338
+ if (!isSequentialComponentRefRead(comp))
339
+ return;
340
+ if (isFullyTyped(comp))
341
+ return;
342
+ 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).`);
343
+ }
344
+ // ── native ports struct + PortReader ──────────────────────────────────────────────────
345
+ function emitPortsStruct(comp, node) {
346
+ const structName = portsStructName(comp.name, node.id);
347
+ const portNames = Object.keys(node.ports);
348
+ if (portNames.length === 0) {
349
+ return `// ${structName} — native ports for node '${node.id}' (${node.component}); no ports.\ntype ${structName} struct{}`;
350
+ }
351
+ const fieldTypes = portNames.map((p) => portFieldGoType(node.ports[p], comp.inputPorts));
352
+ const fieldNames = portNames.map(goPortFieldName);
353
+ const fields = portNames
354
+ .map((p, i) => `\t${fieldNames[i]} ${fieldTypes[i]} // ${JSON.stringify(p)}`)
355
+ .join("\n");
356
+ // NO PortReader `Port(name)` method: the covered handler reads the TYPED struct fields (ports.F_PK)
357
+ // DIRECTLY, so the boxed-Value PortReader impl (the last per-port `dslcontracts.Value` wrap on the
358
+ // covered plane) is DEAD — dropping it makes the ports struct fully native (bc#90 / runtime-free).
359
+ return `// ${structName} — NATIVE ports for node '${node.id}' (${node.component}). Typed fields per the
360
+ // static port type; built by direct field assignment (no map alloc, no per-port boxing). The consumer
361
+ // reads the typed fields DIRECTLY (no PortReader / boxed-Value accessor) — the covered read is native.
362
+ type ${structName} struct {
363
+ ${fields}
364
+ }`;
365
+ }
366
+ // ── CONCRETE per-node row structs + row→outType copiers (bc: full Go concreteness) ──────────
367
+ //
368
+ // The covered path no longer crosses a generic `RawValue`/`RawRow` on the RESULT plane. Each
369
+ // covered node has a CONCRETE row struct `RawRow_<comp>_<node>` whose fields are the node's
370
+ // projected outType fields (typed, flattened) plus a per-node error signal (IsError/Err). The
371
+ // per-component `Handler_<comp>` interface has one method per node returning that concrete struct;
372
+ // the runner reads `row.<Field>` DIRECTLY (no `.(RawRow)`, no `.(scalar)`, no `RawValue`) and copies
373
+ // each field into the node's outType struct local. The `.(T)` type-assert seam that the generic
374
+ // RawValue de-box carried is GONE on the covered path (the handler produces the typed fields).
375
+ //
376
+ // A scalar/arr/opt node (outType not a named struct) has a single `Val <typed>` payload field. A
377
+ // named-struct node has one Go field per outType field (same names/types as the outType decl), so
378
+ // `RawRow_<comp>_<node>` is structurally the outType struct + {IsError, Err}. The copier is then a
379
+ // mechanical field-for-field assignment into the outType struct — NO dynamic value read anywhere.
380
+ /** Row_<comp>_<node> — the concrete per-node handler-result row struct name. (Deliberately NOT
381
+ * prefixed with the generic dynamic-accessor name — that accessor is what the concrete path removes.) */
382
+ function rawRowStructName(compName, nodeId) {
383
+ return `Row_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
384
+ }
385
+ /** RowElem_<comp>_<node> — the concrete per-ELEMENT row struct name for a covered map node (the
386
+ * element/into row the map handler yields per over element). */
387
+ function rawElemStructName(compName, nodeId) {
388
+ return `RowElem_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
389
+ }
390
+ /** Handler_<comp> — the per-component concrete handler interface name (one Node_* method per node). */
391
+ function handlerIfaceName(compName) {
392
+ return `Handler_${sanitize(compName)}`;
393
+ }
394
+ /** Node_<comp>_<nodeId> — the concrete per-node handler method name. */
395
+ function nodeMethodName(compName, nodeId) {
396
+ return `Node_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
397
+ }
398
+ /**
399
+ * emitRawRowStructs — emit the concrete `RawRow_<comp>_<node>` struct per covered node (and, for a
400
+ * covered map node, the per-element `RawElem_<comp>_<node>`). A named-struct outType flattens to one
401
+ * Go field per outType field; a scalar/arr/opt node carries a single `Val` payload. Every row carries
402
+ * the per-node error signal (IsError/Err) so the runner interprets policy without a boxed outcome.
403
+ */
404
+ function emitRawRowStructs(comp, typedNodes, plan) {
405
+ const parts = [];
406
+ for (const n of comp.body) {
407
+ const ref = typedNodes.get(n.id);
408
+ if ("map" in n) {
409
+ // a covered map node: RawElem_<comp>_<node> is the HANDLER'S per-element return shape:
410
+ // - into: the `into` field's type (the fetched item merged onto every over element);
411
+ // - no-into (#93 shape 3): the element outType directly (connection-collect).
412
+ // The batched Node_* returns RawRow_<comp>_<node> (aligned []RawElem_ + error); a per-element
413
+ // dispatch Node_* returns RawElem_ directly (one physical call per element).
414
+ const arrRef = ref;
415
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
416
+ parts.push(emitOneRowStruct(rawElemStructName(comp.name, n.id), elemRowRef, plan));
417
+ parts.push(`// ${rawRowStructName(comp.name, n.id)} — the BATCH row for map '${n.id}': the aligned per-element rows.\n` +
418
+ `type ${rawRowStructName(comp.name, n.id)} struct {\n\tIsError bool\n\tErr string\n\tRows []${rawElemStructName(comp.name, n.id)}\n}`);
419
+ continue;
420
+ }
421
+ parts.push(emitOneRowStruct(rawRowStructName(comp.name, n.id), ref, plan));
422
+ }
423
+ return parts.join("\n\n");
424
+ }
425
+ /** Emit one concrete row struct: flatten a named outType to typed fields, else a single Val payload;
426
+ * always carry IsError/Err. */
427
+ function emitOneRowStruct(name, ref, plan) {
428
+ const lines = [];
429
+ lines.push(`// ${name} — CONCRETE handler-result row (typed fields = the node's outType; + error signal).`);
430
+ lines.push(`type ${name} struct {`);
431
+ lines.push(`\tIsError bool`);
432
+ lines.push(`\tErr string`);
433
+ if (ref.kind === "named") {
434
+ const decl = findDecl(plan, ref.name);
435
+ for (const f of decl.fields) {
436
+ lines.push(`\t${goFieldName(f.name)} ${renderTypeRef(f.type)} // ${JSON.stringify(f.name)}`);
437
+ }
438
+ }
439
+ else {
440
+ lines.push(`\tVal ${renderTypeRef(ref)}`);
441
+ }
442
+ lines.push(`}`);
443
+ return lines.join("\n");
444
+ }
445
+ /** emitRowCopy — the statements copying a concrete row's typed fields into the node's outType local
446
+ * `target` (a value of Go type renderTypeRef(ref)). Named: field-for-field; scalar/arr/opt: `.Val`.
447
+ * `rowExpr` is the concrete RawRow_/RawElem_ value; `indent` tabs. No dynamic read — pure assignment. */
448
+ function emitRowCopy(target, rowExpr, ref, plan, indent) {
449
+ const t = "\t".repeat(indent);
450
+ if (ref.kind === "named") {
451
+ const decl = findDecl(plan, ref.name);
452
+ return decl.fields.map((f) => `${t}${target}.${goFieldName(f.name)} = ${rowExpr}.${goFieldName(f.name)}`).join("\n");
453
+ }
454
+ return `${t}${target} = ${rowExpr}.Val`;
455
+ }
456
+ /**
457
+ * boundGoType (change #3 — typed `bound`) — the CONCRETE Go type of a covered node's `bound` handler
458
+ * param (bc#90 / runtime-free). `bound` is the parent-produced key (the bindField value); it replaces
459
+ * the old boxed `dslcontracts.Value`. We represent PRODUCED-vs-UNPRODUCED with a POINTER: a non-nil
460
+ * `*T` means the parent value was produced/present (an empty string is a VALID produced value — a
461
+ * non-nil `*string` to ""); a nil pointer means UNPRODUCED/skip (exactly the old `!= nil` guard ≡
462
+ * run_behavior). The pointed-to type is the bindField's concrete Go type (a string key here). A node
463
+ * WITHOUT a bindField never receives a bound value — its param is a `*string` placeholder always
464
+ * passed `nil` (a typed nil, so the ABI stays uniform and carries NO dslcontracts.Value).
465
+ */
466
+ function boundGoType(node, comp, typedNodes, plan) {
467
+ if (node.bindField === undefined || node.parent === undefined)
468
+ return "*string";
469
+ const parentRef = typedNodes.get(node.parent);
470
+ if (parentRef === undefined)
471
+ return "*string";
472
+ let bfRef;
473
+ try {
474
+ bfRef = goTypedInternals.typedFieldAccess(typedLocal(node.parent), parentRef, [node.bindField], plan).ref;
475
+ }
476
+ catch {
477
+ return "*string";
478
+ }
479
+ // opt bindField: the field is already a *T — the bound pointed-to type is the inner. A required
480
+ // scalar bindField: the bound pointed-to type is that scalar's Go type.
481
+ if (bfRef.kind === "opt")
482
+ return `*${renderTypeRef(bfRef.inner)}`;
483
+ return `*${renderTypeRef(bfRef)}`;
484
+ }
485
+ /**
486
+ * emitHandlerInterface — the per-component `Handler_<comp>` interface: one concrete-typed `Node_*`
487
+ * method per covered node. Each componentRef node method takes the node's native ports struct + the
488
+ * typed `bound` pointer (change #3 — the produced-aware parent key; NO boxed dslcontracts.Value) and
489
+ * returns its concrete row struct + a resolved bool. A covered map node's method takes the batch
490
+ * ports struct (batched) or the element ports struct (per-element) and returns the batch / element
491
+ * row. This is the fully-concrete, runtime-free replacement for the generic ExecNativeRaw seam.
492
+ */
493
+ function emitHandlerInterface(comp, typedNodes, plan) {
494
+ const lines = [];
495
+ lines.push(`// ${handlerIfaceName(comp.name)} — the CONCRETE per-component handler seam: one typed method`);
496
+ lines.push(`// per covered node (native ports struct IN, concrete row struct OUT). No generic boxed-ports`);
497
+ lines.push(`// / boxed-value / dynamic accessor crosses the covered boundary — the consumer implements each`);
498
+ lines.push(`// Node_* with a decode of its own wire payload into the concrete row (see INTEGRATION.md §6).`);
499
+ lines.push(`type ${handlerIfaceName(comp.name)} interface {`);
500
+ for (const n of comp.body) {
501
+ const method = nodeMethodName(comp.name, n.id);
502
+ const bt = boundGoType(n, comp, typedNodes, plan);
503
+ if ("map" in n) {
504
+ const m = n.map;
505
+ const batched = m.batched === true;
506
+ const portsT = batched ? `${portsStructName(comp.name, n.id)}_batch` : portsStructName(comp.name, n.id);
507
+ const retT = batched ? rawRowStructName(comp.name, n.id) : rawElemStructName(comp.name, n.id);
508
+ lines.push(`\t${method}(ports ${portsT}, bound ${bt}) (${retT}, bool)`);
509
+ continue;
510
+ }
511
+ lines.push(`\t${method}(ports ${portsStructName(comp.name, n.id)}, bound ${bt}) (${rawRowStructName(comp.name, n.id)}, bool)`);
512
+ }
513
+ lines.push(`}`);
514
+ return lines.join("\n");
515
+ }
516
+ /** mapElemMaterializerName — the augmented-element copier for a covered map...into node. */
517
+ function mapElemMaterializerName(compName, nodeId) {
518
+ return `materializeMapElemNR_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}`;
519
+ }
520
+ /**
521
+ * emitMapPortsStructs — the per-element native ports struct + the batch struct for a covered map
522
+ * node (bc#90 / runtime-free). The element struct is the same shape as a componentRef ports struct
523
+ * (typed fields per the static port type). The batch struct carries `Items []<elemStruct>` — a NATIVE
524
+ * slice of the concrete per-element ports structs (NO `[]dslcontracts.Value`, NO PortReader accessor);
525
+ * the consumer reads `bp.Items[i].F_PK` directly.
526
+ */
527
+ function emitMapPortsStructs(comp, node) {
528
+ const m = node.map;
529
+ const structName = portsStructName(comp.name, node.id);
530
+ const elemStruct = emitPortsStruct(comp, { id: node.id, component: m.component, ports: m.ports });
531
+ const batchStruct = `${structName}_batch`;
532
+ return `${elemStruct}
533
+
534
+ // ${batchStruct} — NATIVE batched ports for map '${node.id}': Items is a native slice of the concrete
535
+ // per-element ports structs (${structName}). The consumer reads Items[i].<Field> DIRECTLY — no boxed
536
+ // []Value, no PortReader accessor, no key-value object (bc#90 / runtime-free).
537
+ type ${batchStruct} struct {
538
+ Items []${structName}
539
+ }`;
540
+ }
541
+ /** mapElemRowRef — the handler's per-element RETURN shape for a covered map node: the `into` field's
542
+ * type (into) or the element outType directly (no-into). This is the type of RawElem_<comp>_<node>. */
543
+ function mapElemRowRef(node, arrRef, plan) {
544
+ const m = node.map;
545
+ if (m.into === undefined)
546
+ return arrRef.elem;
547
+ if (arrRef.elem.kind !== "named") {
548
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): node '${node.id}' element outType must be a named struct (got ${JSON.stringify(arrRef.elem.kind)}) — the augmented element de-boxes into a struct. Regenerate as boxed typed/straight-line.`);
549
+ }
550
+ const decl = findDecl(plan, arrRef.elem.name);
551
+ const intoField = decl.fields.find((f) => f.name === m.into);
552
+ if (!intoField) {
553
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): element struct '${arrRef.elem.name}' has no '${m.into}' (into) field.`);
554
+ }
555
+ return intoField.type;
556
+ }
557
+ /**
558
+ * emitMapElemMaterializers — for each covered map...into node, emit the AUGMENTED-element copier: copy
559
+ * the over element's typed fields + assign the `into` field from the concrete per-element handler row
560
+ * (RawElem_<comp>_<node>). Pure struct field assignment — NO dynamic value read (the handler already
561
+ * produced the typed into row).
562
+ */
563
+ function emitMapElemMaterializers(comp, typedNodes, plan) {
564
+ const parts = [];
565
+ for (const n of comp.body) {
566
+ if (!("map" in n))
567
+ continue;
568
+ const m = n.map;
569
+ // #93 shape 3: a map with NO `into` collects each element DIRECTLY from the per-element handler row
570
+ // (RawElem_ IS the element outType) — no over-element merge, so no augmented-element copier here.
571
+ if (m.into === undefined)
572
+ continue;
573
+ const arrRef = typedNodes.get(n.id);
574
+ const elemRef = arrRef.elem;
575
+ const decl = findDecl(plan, elemRef.name);
576
+ const intoKey = m.into;
577
+ // over element type (all element fields EXCEPT the into field): these come from the over element
578
+ // struct (the parent collection element). We copy each by same-named field access.
579
+ const overFields = decl.fields.filter((f) => f.name !== intoKey);
580
+ const intoRowRef = mapElemRowRef(n, arrRef, plan);
581
+ const fn = mapElemMaterializerName(comp.name, n.id);
582
+ const overElemGo = mapOverElemType(comp, n, typedNodes, plan);
583
+ const elemRowGo = rawElemStructName(comp.name, n.id);
584
+ const lines = [];
585
+ lines.push(`// ${fn} — assemble the augmented map element '${n.id}': copy the over element's typed`);
586
+ lines.push(`// fields + set the '${intoKey}' (into) field from the concrete per-element handler row.`);
587
+ lines.push(`func ${fn}(over ${overElemGo}, into ${elemRowGo}) ${elemRef.name} {`);
588
+ lines.push(`\tvar out ${elemRef.name}`);
589
+ for (const f of overFields) {
590
+ lines.push(`\tout.${goFieldName(f.name)} = over.${goFieldName(f.name)}`);
591
+ }
592
+ lines.push(emitRowCopy(`out.${goFieldName(intoKey)}`, "into", intoRowRef, plan, 1));
593
+ lines.push(`\treturn out`);
594
+ lines.push(`}`);
595
+ parts.push(lines.join("\n"));
596
+ }
597
+ return parts.join("\n\n");
598
+ }
599
+ /** The Go type of the over-element (the parent collection element) for a covered map node. `over`
600
+ * is a static ref to a prior node's arr<named> field (or an input array). We resolve it via the
601
+ * parent node's typed struct field type. */
602
+ function mapOverElemType(comp, node, typedNodes, plan) {
603
+ const m = node.map;
604
+ const e = classifyExpr(m.over);
605
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
606
+ const baseRef = typedNodes.get(e.path[0]);
607
+ const { ref } = goTypedInternals.typedFieldAccess(typedLocal(e.path[0]), baseRef, e.path.slice(1), plan);
608
+ if (ref.kind === "arr")
609
+ return renderTypeRef(ref.elem);
610
+ }
611
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined map (#86 pt2): map '${node.id}' 'over' must be a ref to a prior node's typed arr field (got ${JSON.stringify(m.over)}).`);
612
+ }
613
+ function classifyPort(node) {
614
+ if (staticArrElems(node) !== null)
615
+ return "static";
616
+ const e = classifyExpr(node);
617
+ switch (e.kind) {
618
+ case "str":
619
+ case "bool":
620
+ case "null":
621
+ case "ref":
622
+ return "static";
623
+ case "concat":
624
+ return e.parts.every((p) => classifyPort(reconstructG(p)) === "static") ? "static" : "dynamic";
625
+ default:
626
+ return "dynamic";
627
+ }
628
+ }
629
+ function reconstructG(e) {
630
+ switch (e.kind) {
631
+ case "str":
632
+ return e.value;
633
+ case "bool":
634
+ return e.value;
635
+ case "null":
636
+ return null;
637
+ case "ref":
638
+ return { [e.opt ? "refOpt" : "ref"]: e.path };
639
+ case "concat":
640
+ return { concat: e.parts.map(reconstructG) };
641
+ case "dynamic":
642
+ return e.node;
643
+ }
644
+ }
645
+ // bc#90 / runtime-free: the old nvVE-based `emitStaticVE` / `valueLeaf` port lowering (which built
646
+ // `dslcontracts.Value` leaves via the nv* helpers) is REMOVED — every covered port now lowers to a
647
+ // FULLY NATIVE Go value in emitPortInit (string / bool / static-string-array / number literal), and a
648
+ // genuinely-dynamic port fails closed. There is no boxed-Value fallback on the covered read plane.
649
+ // ── typed input struct (In_<comp>) + NATIVE scalar port lowering ─────────────────────────────
650
+ //
651
+ // A covered runner takes a CONCRETE per-component input struct `In_<comp>` (fields = the component's
652
+ // inputPorts with their declared Go types) instead of a generic *Obj. A simple string/scalar port —
653
+ // a literal, a ref to a typed input field (`in.UserId`), a ref to a prior node's typed struct scalar
654
+ // field, or a concat of statically-string parts — then lowers DIRECTLY to a native Go expression
655
+ // (`"USER#" + in.UserId`) assigned straight into the concrete ports-struct field: NO dslcontracts.Value,
656
+ // NO nvVE/nvBindVE helper, NO `.(string)` type-assert. The value is statically the concrete type (the
657
+ // input schema / prior-node outType guarantee it, and literals are self-evidently typed), so dropping
658
+ // the runtime TYPE_MISMATCH check is semantics-preserving. A port that CANNOT be statically proven the
659
+ // concrete type (a ref to a non-matching-typed input, a refOpt / opt field, or a dynamic operator)
660
+ // falls back to the nvVE Value path (documented — the only place a `.(T)` could survive; the covered
661
+ // string key/table/projection ports never hit it).
662
+ /** In_<comp> — the concrete per-component input struct name (fields = inputPorts). */
663
+ function inStructName(compName) {
664
+ return `In_${sanitize(compName)}`;
665
+ }
666
+ /** Go type for an input port's declared type (scalar → concrete; anything else → Value). */
667
+ function inputPortGoType(schema) {
668
+ switch (schema?.type) {
669
+ case "string":
670
+ return "string";
671
+ case "int":
672
+ return "int64";
673
+ case "float":
674
+ return "float64";
675
+ case "bool":
676
+ return "bool";
677
+ default:
678
+ return `${PKG}.Value`;
679
+ }
680
+ }
681
+ /** The scalar kind an input port declares, or undefined if it is not a native scalar. */
682
+ function inputScalarKind(schema) {
683
+ switch (schema?.type) {
684
+ case "string":
685
+ return "string";
686
+ case "int":
687
+ return "int64";
688
+ case "float":
689
+ return "float64";
690
+ case "bool":
691
+ return "bool";
692
+ default:
693
+ return undefined;
694
+ }
695
+ }
696
+ /** emitInStruct — the concrete input struct declaration for a covered component (fields = inputPorts,
697
+ * declaration order = Object key order; gofmt-aligned). Empty inputPorts => empty struct. */
698
+ function emitInStruct(comp) {
699
+ const name = inStructName(comp.name);
700
+ const ports = Object.keys(comp.inputPorts ?? {});
701
+ if (ports.length === 0) {
702
+ return `// ${name} — the CONCRETE input for '${comp.name}' (no input ports).\ntype ${name} struct{}`;
703
+ }
704
+ const names = ports.map((p) => goFieldName(p));
705
+ const types = ports.map((p) => inputPortGoType(comp.inputPorts[p]));
706
+ const nameW = Math.max(0, ...names.map((n) => n.length));
707
+ const typeW = Math.max(0, ...types.map((t) => t.length));
708
+ const fields = ports
709
+ .map((p, i) => `\t${names[i].padEnd(nameW)} ${types[i].padEnd(typeW)} // ${JSON.stringify(p)}`)
710
+ .join("\n");
711
+ return `// ${name} — the CONCRETE input for '${comp.name}' (fields = inputPorts; typed, consumer-built —
712
+ // NO generic *Obj, NO per-field boxing crosses the covered read boundary).
713
+ type ${name} struct {
714
+ ${fields}
715
+ }`;
716
+ }
717
+ /**
718
+ * emitInDecoder — the TEST-glue decoder `decode_In_<comp>(input *Obj) (In_<comp>, error)`. It reads
719
+ * each declared input port from the generic *Obj into the concrete In_<comp> field (scalar → typed
720
+ * read with a `.(T)` on the SDK/wire Value; Value-typed field → the raw Value). Lives ONLY in the
721
+ * observe companion (test harness), never in the covered module — the covered runner takes In_<comp>
722
+ * pre-built by the consumer. A missing key leaves the zero value (every covered input is required +
723
+ * always supplied by the corpus); a present-but-wrong-scalar is a fail-closed decode error.
724
+ */
725
+ function emitInDecoder(comp) {
726
+ const name = inStructName(comp.name);
727
+ const ports = Object.keys(comp.inputPorts ?? {});
728
+ const lines = [];
729
+ lines.push(`func decode_${name}(input *${PKG}.Obj) (${name}, error) {`);
730
+ lines.push(`\tvar in ${name}`);
731
+ if (ports.length === 0 || comp.inputPorts === undefined) {
732
+ lines.push(`\t_ = input`);
733
+ lines.push(`\treturn in, nil`);
734
+ lines.push(`}`);
735
+ return lines.join("\n");
736
+ }
737
+ lines.push(`\tif input == nil {`);
738
+ lines.push(`\t\treturn in, nil`);
739
+ lines.push(`\t}`);
740
+ for (const p of ports) {
741
+ const field = goFieldName(p);
742
+ const scalar = inputScalarKind(comp.inputPorts[p]);
743
+ lines.push(`\tif v, ok := input.Get(${JSON.stringify(p)}); ok {`);
744
+ if (scalar === undefined) {
745
+ lines.push(`\t\tin.${field} = v`);
746
+ }
747
+ else {
748
+ lines.push(`\t\tt, tok := v.(${scalar})`);
749
+ lines.push(`\t\tif !tok {`);
750
+ lines.push(`\t\t\treturn in, ${PKG}.NewExprFailure("TYPE_MISMATCH", "input ${p}: expected ${scalar}, got "+${PKG}.TypeName(v))`);
751
+ lines.push(`\t\t}`);
752
+ lines.push(`\t\tin.${field} = t`);
753
+ }
754
+ lines.push(`\t}`);
755
+ }
756
+ lines.push(`\treturn in, nil`);
757
+ lines.push(`}`);
758
+ return lines.join("\n");
759
+ }
760
+ /**
761
+ * emitNativeScalar — lower a static port expr to a NATIVE Go expression of the concrete `want` scalar
762
+ * type, or null if it cannot be statically proven that type. Literals: a string literal is native for
763
+ * `want==="string"`, a bool literal for `want==="bool"`. A ref resolves via `resolveRef` (typed input
764
+ * field / prior-node scalar / element binding). A concat lowers ONLY into `string` and only when EVERY
765
+ * part is itself a native string (so the concat's strings-only TYPE_MISMATCH can never fire — the parts
766
+ * are statically strings), emitting `p0 + p1 + …`. Returns null (fall back to the Value path) otherwise.
767
+ */
768
+ function emitNativeScalar(node, want, resolveRef) {
769
+ // a static arr is a Value port (never a scalar) — not native-lowerable here.
770
+ if (staticArrElems(node) !== null)
771
+ return null;
772
+ const e = classifyExpr(node);
773
+ switch (e.kind) {
774
+ case "str":
775
+ return want === "string" ? JSON.stringify(e.value) : null;
776
+ case "bool":
777
+ return want === "bool" ? (e.value ? "true" : "false") : null;
778
+ case "null":
779
+ return null; // a null literal is a Value(nil), never a required native scalar
780
+ case "ref": {
781
+ const sc = resolveRef(e.path[0], e.path.slice(1), e.opt);
782
+ if (sc === null || sc.scalar !== want)
783
+ return null;
784
+ return sc.expr;
785
+ }
786
+ case "concat": {
787
+ if (want !== "string")
788
+ return null;
789
+ const parts = [];
790
+ for (const p of e.parts) {
791
+ const sub = emitNativeScalar(reconstructG(p), "string", resolveRef);
792
+ if (sub === null)
793
+ return null;
794
+ // parenthesize a `+` sub-expression so string concatenation binds correctly (a ref/literal is
795
+ // atomic; a nested concat is already a single `a + b` — wrap to keep it one operand).
796
+ parts.push(sub.includes(" + ") ? `(${sub})` : sub);
797
+ }
798
+ return parts.join(" + ");
799
+ }
800
+ default:
801
+ return null;
802
+ }
803
+ }
804
+ /**
805
+ * emitPortInit — emit the port field build for one port as a FULLY NATIVE Go expression (bc#90 /
806
+ * runtime-free). Every covered port lowers to a concrete Go value assigned straight into the ports
807
+ * struct field — NO `dslcontracts.Value`, NO nvVE helper, NO `.(T)` type-assert:
808
+ * - a static string array (projection) → `[]string{"a", "b", ...}` (change #2);
809
+ * - a bare int/float number literal (e.g. Query `limit: 20`) → `int64(20)` / `float64(...)`;
810
+ * - a string/bool/scalar-input port → the native scalar expression (emitNativeScalar).
811
+ * A port that CANNOT lower natively FAILS CLOSED (there is no boxed Value fallback on the covered
812
+ * plane — a boxed escape would re-introduce the bc-runtime coupling this module removes). Returns the
813
+ * struct field initializer (`Field: <expr>`). `resolveRef` resolves a ref head for the scalar path.
814
+ */
815
+ function emitPortInit(pn, expr, fieldGoType, resolveRef) {
816
+ const fieldName = goPortFieldName(pn);
817
+ // change #2: a static string array (projection) → a native `[]string{...}` literal.
818
+ const strArr = staticStringArrElems(expr);
819
+ if (strArr !== null) {
820
+ if (fieldGoType !== "[]string") {
821
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a static string array but its field type is '${fieldGoType}' (expected []string).`);
822
+ }
823
+ return `${fieldName}: []string{${strArr.map((s) => JSON.stringify(s)).join(", ")}}`;
824
+ }
825
+ // a bare number literal (e.g. Query `limit: 20`): the SSoT range check ran in numLiteralGoExpr —
826
+ // emit the concrete native int64/float64 (unwrap the dslcontracts.Value(...) box: it was only there
827
+ // for the old boxed path; the range check is what matters, and it already passed).
828
+ const num = numLiteralGoExpr(expr);
829
+ if (num !== null) {
830
+ if (fieldGoType === "int64")
831
+ return `${fieldName}: int64(${expr})`;
832
+ if (fieldGoType === "float64")
833
+ return `${fieldName}: float64(${expr})`;
834
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90): port '${pn}' is a number literal but its field type is '${fieldGoType}'.`);
835
+ }
836
+ // string / bool / scalar-input / concat → native scalar expression (no Value, no `.(T)`).
837
+ const scalarWant = fieldGoType;
838
+ const native = emitNativeScalar(expr, scalarWant, resolveRef);
839
+ if (native !== null) {
840
+ return `${fieldName}: ${native}`;
841
+ }
842
+ // FAIL CLOSED: the covered read plane is runtime-free — no boxed Value fallback (that would
843
+ // re-couple the covered module to bc-runtime). If a genuinely-dynamic port reaches here, the
844
+ // component is not native-eligible and must regenerate on the boxed straight-line path.
845
+ throw new GeneratorFailure("UNSUPPORTED_NODE_STRAIGHTLINE", `go combined read (bc#90 / runtime-free): port '${pn}' (${JSON.stringify(expr)}) does not lower to a native Go value of type '${fieldGoType}'. The covered read plane admits NO boxed dslcontracts.Value escape — regenerate on the boxed straight-line path if this port is genuinely dynamic.`);
846
+ }
847
+ /**
848
+ * emitParallelStageArm (bc#87) — EXPLICIT static parallel orchestration for a real-concurrency stage.
849
+ * The stage's members are statically known (from plan.groups), each a plain componentRef point read.
850
+ * We reproduce plan.go's bounded-parallel protocol EXACTLY, so the observed value / emitted op multiset
851
+ * / failure PRECEDENCE / skip reps byte-match run_behavior:
852
+ *
853
+ * 1. PREFLIGHT (ascending index): for each member, decide run/skip from the SETTLED prior-stage
854
+ * results (a plain `produced_<parent>` gate — members are mutually independent, §1) and build its
855
+ * native ports struct. The ports are computed BEFORE dispatch (never inside the goroutine) so the
856
+ * order-of-evaluation is deterministic and a port ExprFailure surfaces in ascending index order.
857
+ * 2. DISPATCH (bounded): one `go func(){…}()` per RUNNABLE member, bound to the static plan.concurrency
858
+ * via a semaphore channel of known size (min(concurrency, member count)); a sync.WaitGroup joins
859
+ * them. Each goroutine ONLY calls the handler and stores its RawOutcome into a per-member struct
860
+ * local — the goroutine spawn is the SOLE runtime element.
861
+ * 3. COMMIT (ascending index): interpret Policy Kind + materialize the RawValue into the outType
862
+ * struct, in ascending index order. Under 'fail'/'retry', the FIRST (lowest-index) failing member
863
+ * wins — a race can NEVER change which failure surfaces, because interpretation is committed
864
+ * deterministically after all execs settle (collect-then-apply, mirroring plan.go's commit loop).
865
+ *
866
+ * NO RunPlan / []OpSpec / planSpec walk: the staging, the bound, and which ops are parallel are baked.
867
+ */
868
+ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt) {
869
+ const lines = [];
870
+ const ids = stage.map((i) => comp.body[i].id);
871
+ lines.push(`\t// ── bc#87 real-concurrency stage {${ids.map((x) => JSON.stringify(x)).join(", ")}} — static parallel`);
872
+ lines.push(`\t// orchestration (goroutine per op, bound=${concurrency}); preflight+commit in ascending index order.`);
873
+ lines.push(`\t{`);
874
+ // per-member outcome + resolved-flag + ports locals, declared at STAGE scope (assigned in the
875
+ // ascending preflight, read at dispatch/commit — a goroutine closes over the ports local).
876
+ for (const i of stage) {
877
+ const s = sanitize(comp.body[i].id);
878
+ const structName = portsStructName(comp.name, comp.body[i].id);
879
+ const rowT = rawRowStructName(comp.name, comp.body[i].id);
880
+ lines.push(`\t\tvar row_${s} ${rowT}`);
881
+ lines.push(`\t\tvar row_${s}Resolved bool`);
882
+ lines.push(`\t\trun_${s} := false`);
883
+ lines.push(`\t\tvar ps_${s} ${structName}`);
884
+ lines.push(`\t\t_ = ps_${s}`);
885
+ }
886
+ // 1) PREFLIGHT (ascending): parent-produced gate + ports build, per member (BEFORE dispatch).
887
+ for (const i of stage) {
888
+ const node = comp.body[i];
889
+ const s = sanitize(node.id);
890
+ const structName = portsStructName(comp.name, node.id);
891
+ let ind = "\t\t";
892
+ const closers = [];
893
+ if (node.parent !== undefined && priorNodeAt(node.parent, atPos)) {
894
+ lines.push(`${ind}if produced_${sanitize(node.parent)} {`);
895
+ closers.unshift(`${ind}}`);
896
+ ind += "\t";
897
+ }
898
+ const inits = [];
899
+ const resolveRef = resolveRefAt(atPos);
900
+ for (const pn of Object.keys(node.ports)) {
901
+ const expr = node.ports[pn];
902
+ const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
903
+ inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
904
+ }
905
+ lines.push(`${ind}ps_${s} = ${structName}{${inits.join(", ")}}`);
906
+ lines.push(`${ind}run_${s} = true`);
907
+ for (const c of closers)
908
+ lines.push(c);
909
+ }
910
+ // 2) DISPATCH (bounded): one goroutine per runnable member, bound by a semaphore of the static size.
911
+ const bound = Math.min(concurrency, stage.length);
912
+ lines.push(`\t\tvar wg sync.WaitGroup`);
913
+ lines.push(`\t\tsem := make(chan struct{}, ${bound})`);
914
+ for (const i of stage) {
915
+ const node = comp.body[i];
916
+ const s = sanitize(node.id);
917
+ lines.push(`\t\tif run_${s} {`);
918
+ lines.push(`\t\t\twg.Add(1)`);
919
+ lines.push(`\t\t\tsem <- struct{}{}`);
920
+ lines.push(`\t\t\tgo func() {`);
921
+ lines.push(`\t\t\t\tdefer wg.Done()`);
922
+ lines.push(`\t\t\t\tdefer func() { <-sem }()`);
923
+ lines.push(`\t\t\t\trow_${s}, row_${s}Resolved = handlers.${nodeMethodName(comp.name, node.id)}(ps_${s}, nil)`);
924
+ lines.push(`\t\t\t}()`);
925
+ lines.push(`\t\t}`);
926
+ }
927
+ lines.push(`\t\twg.Wait()`);
928
+ // 3) COMMIT (ascending index): interpret + materialize deterministically — lowest-index failure wins.
929
+ for (const i of stage) {
930
+ const node = comp.body[i];
931
+ const meta = metas[i];
932
+ const s = sanitize(node.id);
933
+ const ref = typedNodes.get(node.id);
934
+ const oc = `row_${s}`;
935
+ lines.push(`\t\tif run_${s} {`);
936
+ 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}`);
937
+ if (meta.policy === "continue") {
938
+ lines.push(`\t\t\tif !${oc}.IsError {`);
939
+ lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 4));
940
+ lines.push(`\t\t\t\tproduced_${s} = true`);
941
+ lines.push(`\t\t\t}`);
942
+ }
943
+ else {
944
+ const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
945
+ lines.push(`\t\t\tif ${oc}.IsError {`);
946
+ lines.push(`\t\t\t\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
947
+ lines.push(`\t\t\t}`);
948
+ lines.push(emitRowCopy(typedLocal(node.id), oc, ref, plan, 3));
949
+ lines.push(`\t\t\tproduced_${s} = true`);
950
+ }
951
+ lines.push(`\t\t}`);
952
+ }
953
+ lines.push(`\t}`);
954
+ return lines.join("\n");
955
+ }
956
+ /**
957
+ * emitNativeRawRunner — the combined read exec for one component, as a STRUCT-RETURNING runner
958
+ * (the fully de-plumbed path): native ports struct IN (direct field assignment), raw struct result
959
+ * OUT (marshalRaw_* straight into the node's outType struct — no boxed Value on the row plane),
960
+ * INLINE sequential (NO RunPlan). Node results are TYPED STRUCT locals; a relation child reads the
961
+ * parent's REAL struct result inline (typed field access — no serialize), and the child-present /
962
+ * bindField / continue-skip decision is made from the real parent value — so relationSingle /
963
+ * connection converge with run_behavior. The output is assembled as a TYPED STRUCT/value (via the
964
+ * typed output lowering — struct literal + struct-field access, ZERO NewObj/.Set). For a covered
965
+ * read whose ports/output are input-static + cross-node refs, this runner allocates ZERO *Obj: the
966
+ * consumer (graphddb) drives it and keeps the struct native. A Value observation is produced ONLY
967
+ * by the separate serialize wrapper (run_native_raw_<comp>, below), the single outer Value boundary.
968
+ */
969
+ function emitNativeRawRunner(comp, plan, flags) {
970
+ const ep = execPlan(comp);
971
+ const order = ep.order;
972
+ const cplan = buildComponentPlan(comp, goStraightlineInternals.dialect, "scope");
973
+ const metas = analyzeSequentialOps(cplan.opsLiteral, order);
974
+ const fn = sanitize(comp.name);
975
+ const typedNodes = new Map();
976
+ for (const n of comp.body)
977
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
978
+ const idToPos = new Map();
979
+ order.forEach((opIdx, k) => idToPos.set(comp.body[opIdx].id, k));
980
+ const priorNodeAt = (head, atPos) => {
981
+ const p = idToPos.get(head);
982
+ return p !== undefined && p < atPos;
983
+ };
984
+ const outRef = comp.outputType !== undefined ? deriveTypeRef(comp.outputType, plan) : undefined;
985
+ const outStructType = outRef ? renderTypeRef(outRef) : `${PKG}.Value`;
986
+ const zeroOut = outRef ? goZero(outRef) : "nil";
987
+ const lines = [];
988
+ lines.push(`// run_native_raw_struct_${fn} — the STRUCT-RETURNING combined read (bc#77/#87): the fully`);
989
+ lines.push(`// de-plumbed path. Ports are a native struct (direct field assignment); the handler`);
990
+ lines.push(`// result is materialized straight into the node's outType struct. Node results are typed`);
991
+ lines.push(`// struct locals; a relation child reads the parent's REAL struct result via direct field`);
992
+ lines.push(`// access (child-present decision from the real parent value — relationSingle / connection`);
993
+ lines.push(`// converge). A strictly-sequential plan is a plain statement list (no plan driver); a`);
994
+ lines.push(`// real-concurrency stage (bc#87) is EXPLICIT static parallel orchestration — one goroutine`);
995
+ lines.push(`// per statically-known parallel op, bounded by the static plan.concurrency via a semaphore`);
996
+ lines.push(`// of known size, with a sync.WaitGroup; preflight + interpret are committed in ascending`);
997
+ lines.push(`// index order so the observed value / op multiset / failure precedence byte-match run_behavior`);
998
+ lines.push(`// (the goroutine spawn is the ONLY runtime element — the WHAT is baked). The output is a typed`);
999
+ lines.push(`// struct/value assembled by struct literal + field access — the consumer keeps it native.`);
1000
+ // FULLY CONCRETE: GENERIC over the per-component CONCRETE handler interface ${handlerIfaceName(comp.name)},
1001
+ // whose Node_* methods return the concrete per-node row struct (typed fields = the node's outType).
1002
+ // The covered result plane carries no boxed ports / boxed value / dynamic accessor / semantic type-
1003
+ // assert on a row value — the runner reads row.<Field> directly and copies each into the outType local.
1004
+ lines.push(`func run_native_raw_struct_${fn}[H ${handlerIfaceName(comp.name)}](handlers H, in ${inStructName(comp.name)}) (${outStructType}, error) {`);
1005
+ lines.push(`\t_ = in`);
1006
+ // resolveNativeRef for this runner: a ref head is either a PRIOR NODE (read its typed struct field
1007
+ // directly) or an INPUT PORT (read the typed `in.<Field>`). A REQUIRED scalar resolves to a native
1008
+ // Go expression; an opt / non-scalar / refOpt resolves to null (falls back to the nvVE Value path).
1009
+ const resolveRefAt = (atPos) => (head, restPath, opt) => {
1010
+ if (opt)
1011
+ return null; // a refOpt may be nil at runtime — keep the Value path (opt-nil semantics)
1012
+ if (priorNodeAt(head, atPos)) {
1013
+ const baseRef = typedNodes.get(head);
1014
+ let acc;
1015
+ let expr;
1016
+ try {
1017
+ const r = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, restPath, plan);
1018
+ expr = r.expr;
1019
+ acc = r.ref;
1020
+ }
1021
+ catch {
1022
+ return null;
1023
+ }
1024
+ if (acc.kind !== "scalar" || acc.scalar === "null")
1025
+ return null;
1026
+ return { expr, scalar: goScalar(acc.scalar) };
1027
+ }
1028
+ if (restPath.length !== 0)
1029
+ return null; // input.<field> path is a Value walk — not a native scalar
1030
+ const scalar = inputScalarKind(comp.inputPorts?.[head]);
1031
+ if (scalar === undefined)
1032
+ return null;
1033
+ return { expr: `in.${goFieldName(head)}`, scalar };
1034
+ };
1035
+ for (const k of order.keys()) {
1036
+ const id = comp.body[order[k]].id;
1037
+ lines.push(`\tvar ${typedLocal(id)} ${renderTypeRef(typedNodes.get(id))}`);
1038
+ lines.push(`\tproduced_${sanitize(id)} := false`);
1039
+ lines.push(`\t_ = ${typedLocal(id)}`);
1040
+ lines.push(`\t_ = produced_${sanitize(id)}`);
1041
+ }
1042
+ const idToPosAll = new Map();
1043
+ order.forEach((opIdx, k) => idToPosAll.set(opIdx, k));
1044
+ for (let k = 0; k < order.length; k++) {
1045
+ const i = order[k];
1046
+ // bc#87: a real-concurrency stage — emit static parallel orchestration for ALL its members here,
1047
+ // then advance past them. The stage members are consecutive in `order` (execPlan concatenates
1048
+ // ascending-sorted stages), so we emit the block at the first member and skip the rest.
1049
+ const stage = ep.parallelStageOf.get(i);
1050
+ if (stage !== undefined && stage[0] === i) {
1051
+ lines.push(emitParallelStageArm(comp, stage, k, ep.concurrency, metas, typedNodes, plan, priorNodeAt, zeroOut, flags, resolveRefAt));
1052
+ k += stage.length - 1;
1053
+ continue;
1054
+ }
1055
+ // #86 pt2: a covered batched map...into node — struct-native element collection + into enrichment.
1056
+ if ("map" in comp.body[i]) {
1057
+ lines.push(emitMapArm(comp, comp.body[i], k, typedNodes, plan, priorNodeAt, zeroOut, flags));
1058
+ continue;
1059
+ }
1060
+ const node = comp.body[i];
1061
+ const meta = metas[i];
1062
+ const s = sanitize(node.id);
1063
+ const structName = portsStructName(comp.name, node.id);
1064
+ const portNames = Object.keys(node.ports);
1065
+ const ref = typedNodes.get(node.id);
1066
+ lines.push(`\t// ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
1067
+ let indent = "\t";
1068
+ const closers = [];
1069
+ if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
1070
+ lines.push(`${indent}if produced_${sanitize(node.parent)} {`);
1071
+ closers.unshift(`${indent}}`);
1072
+ indent += "\t";
1073
+ if (node.bindField !== undefined) {
1074
+ // bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
1075
+ // access — no serialize). `bound_<s>` is a TYPED POINTER (`*T`) — nil => UNPRODUCED/skip, non-nil
1076
+ // => produced (an empty string is a VALID produced value, a non-nil *string to ""). This is the
1077
+ // EXACT `!= nil` skip-gate ≡ run_behavior's preflightOp, now runtime-free (NO dslcontracts.Value).
1078
+ const parentRef = typedNodes.get(node.parent);
1079
+ const { expr: bfExpr, ref: bfRef } = goTypedInternals.typedFieldAccess(typedLocal(node.parent), parentRef, [node.bindField], plan);
1080
+ if (bfRef.kind === "opt") {
1081
+ // an opt bindField is ALREADY a *T: nil => skip, non-nil => produced. Use it directly.
1082
+ lines.push(`${indent}bound_${s} := ${bfExpr}`);
1083
+ }
1084
+ else {
1085
+ // a required scalar bindField is always PRESENT (never skips on null): take its address so
1086
+ // the produced pointer is non-nil, preserving the "produced" semantics of the old guard.
1087
+ lines.push(`${indent}boundV_${s} := ${bfExpr}`);
1088
+ lines.push(`${indent}bound_${s} := &boundV_${s}`);
1089
+ }
1090
+ lines.push(`${indent}if bound_${s} != nil {`);
1091
+ closers.unshift(`${indent}}`);
1092
+ indent += "\t";
1093
+ }
1094
+ }
1095
+ // ports: build the native struct by direct field assignment (bc#90 / runtime-free). Every covered
1096
+ // port lowers to a concrete Go value (literal / input-param ref / cross-node typed field / concat /
1097
+ // static string array / number literal) assigned straight into the ports field — ZERO
1098
+ // dslcontracts.Value, ZERO nvVE helper, ZERO `.(T)`. A genuinely-dynamic port FAILS CLOSED.
1099
+ const inits = [];
1100
+ const boundArg = node.bindField !== undefined ? `bound_${s}` : "nil";
1101
+ const resolveRef = resolveRefAt(k);
1102
+ for (const pn of portNames) {
1103
+ const expr = node.ports[pn];
1104
+ const fieldGoType = portFieldGoType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`);
1105
+ inits.push(emitPortInit(pn, expr, fieldGoType, resolveRef));
1106
+ }
1107
+ lines.push(`${indent}ports_${s} := ${structName}{${inits.join(", ")}}`);
1108
+ const oc = `row_${s}`;
1109
+ lines.push(`${indent}${oc}, ${oc}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ports_${s}, ${boundArg})`);
1110
+ lines.push(`${indent}if !${oc}Resolved {`, `${indent}\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${node.component}' has no handler (fail-closed)"`)}`, `${indent}}`);
1111
+ const copy = (ind) => emitRowCopy(typedLocal(node.id), oc, ref, plan, ind.length);
1112
+ if (meta.policy === "continue") {
1113
+ lines.push(`${indent}if !${oc}.IsError {`);
1114
+ lines.push(copy(indent + "\t"));
1115
+ lines.push(`${indent}\tproduced_${s} = true`);
1116
+ lines.push(`${indent}}`);
1117
+ }
1118
+ else {
1119
+ const policy = meta.policy === "retry" ? "retry' policy (exhausted)" : "fail' policy";
1120
+ lines.push(`${indent}if ${oc}.IsError {`);
1121
+ lines.push(`${indent}\treturn ${zeroOut}, ${goErr("OP_FAILED", `"operation '${node.id}' failed under '${policy}: "+${oc}.Err`)}`);
1122
+ lines.push(`${indent}}`);
1123
+ lines.push(copy(indent));
1124
+ lines.push(`${indent}produced_${s} = true`);
1125
+ }
1126
+ for (const c of closers)
1127
+ lines.push(c);
1128
+ }
1129
+ // output: assemble a TYPED struct/value via the typed output lowering (struct literal + typed
1130
+ // field access — ZERO NewObj/.Set). This is the reused boxed-typed output assembly, but the
1131
+ // result stays a struct (the consumer keeps it native). An input-param output ref would need the
1132
+ // Value-scope fallback (scopeNR) — the covered read corpus does not hit it.
1133
+ //
1134
+ // #86 part 1: for a bindField relationSingle child that can SKIP at runtime, a ref/refOpt into it
1135
+ // must yield the UNPRODUCED rep (run_behavior's writeUnproduced: null for a single child) rather
1136
+ // than the zero-value struct. So when any covered node canSkip, we use the produced-aware lowering
1137
+ // which gates such a ref on the node's produced_<id> flag.
1138
+ const skippable = new Set();
1139
+ const skippableKind = new Map();
1140
+ for (const k of order.keys()) {
1141
+ const bodyNode = comp.body[order[k]];
1142
+ const id = bodyNode.id;
1143
+ if (metas[order[k]].canSkip) {
1144
+ skippable.add(id);
1145
+ // relationKind of the skippable node decides its unproduced rep (#93 shape 1): a connection
1146
+ // yields the empty connection {items:[],cursor:null}; single/absent yields null (opt-nil).
1147
+ const rk = "map" in bodyNode ? bodyNode.map.relationKind : bodyNode.relationKind;
1148
+ skippableKind.set(id, rk);
1149
+ }
1150
+ }
1151
+ const out = skippable.size > 0
1152
+ ? emitProducedAwareValue(comp.output, outRef, typedNodes, plan, skippable, skippableKind)
1153
+ : emitTypedValue(comp.output, outRef, typedNodes, plan);
1154
+ lines.push(`\treturn ${out.expr}, nil`);
1155
+ lines.push(`}`);
1156
+ return { code: lines.join("\n"), outRef: out.ref ?? outRef };
1157
+ }
1158
+ // bc#90 / runtime-free: the old nvVE-based `emitMapStaticVE` map-element port lowering is REMOVED —
1159
+ // covered map element ports lower to a fully native Go value in emitPortInit (the map resolveRef reads
1160
+ // the $as element / prior-node / input typed fields directly), with a fail-closed on a dynamic port.
1161
+ /**
1162
+ * emitMapArm — the struct-native exec of a covered map node. Three shapes:
1163
+ * - #86 pt2: BATCHED map...into (nestedBatchGet) — dispatch once, zip-attach `into` onto each over
1164
+ * element (the augmented element materializer).
1165
+ * - #93 shape 2: NON-batched map...into — dispatch the child handler ONCE PER ELEMENT (N physical
1166
+ * requests, matching run_behavior's per-element loop), materialize `into` per element.
1167
+ * - #93 shape 3: BATCHED map with NO `into` — dispatch once, collect each element DIRECTLY from the
1168
+ * aligned batch raw via the element's own named marshaller (the connection/plain-list collect).
1169
+ * In every case: over is a typed field access (no serialize), element ports are a native struct
1170
+ * (direct field assignment; $as refs read the over element's typed fields), and the result is a
1171
+ * native []T with NO boxed Value on any plane. The map is under the parent's produced gate (inline).
1172
+ */
1173
+ function emitMapArm(comp, node, atPos, typedNodes, plan, priorNodeAt, zeroOut, flags) {
1174
+ const m = node.map;
1175
+ const batched = m.batched === true;
1176
+ const hasInto = m.into !== undefined;
1177
+ const s = sanitize(node.id);
1178
+ const structName = portsStructName(comp.name, node.id);
1179
+ const batchStruct = `${structName}_batch`;
1180
+ const arrRef = typedNodes.get(node.id);
1181
+ const elemGo = renderTypeRef(arrRef);
1182
+ const asName = m.as;
1183
+ const overElemGo = mapOverElemType(comp, node, typedNodes, plan);
1184
+ // over element type ref (for $as field access): resolve the parent field's arr element type.
1185
+ const overE = classifyExpr(m.over);
1186
+ const overParent = overE.kind === "ref" ? overE.path[0] : "";
1187
+ const overBaseRef = typedNodes.get(overParent);
1188
+ const { expr: overExpr, ref: overRef } = goTypedInternals.typedFieldAccess(typedLocal(overParent), overBaseRef, overE.kind === "ref" ? overE.path.slice(1) : [], plan);
1189
+ const overElemRef = overRef.elem;
1190
+ const elemMat = mapElemMaterializerName(comp.name, node.id);
1191
+ const priorHere = (head) => priorNodeAt(head, atPos);
1192
+ const portNames = Object.keys(m.ports);
1193
+ // emit the per-element native ports struct build (shared; `il` is the indent inside the element
1194
+ // loop, `elemVar` the over element loop variable). Returns the lines building `ep_<s>`.
1195
+ const buildPorts = (il, elemVar) => {
1196
+ const out = [];
1197
+ const inits = [];
1198
+ // resolveNativeRef for a map element port: a ref head is the element binding ($as → the over element
1199
+ // typed struct field `elemVar.<Field>`), a PRIOR NODE (its typed struct field), or an INPUT PORT
1200
+ // (`in.<Field>`). A REQUIRED scalar lowers to a native Go expr; opt / non-scalar → null (Value path).
1201
+ const resolveRef = (head, restPath, opt) => {
1202
+ if (opt)
1203
+ return null;
1204
+ let baseExpr;
1205
+ let baseRef;
1206
+ if (head === asName) {
1207
+ baseExpr = elemVar;
1208
+ baseRef = overElemRef;
1209
+ }
1210
+ else if (priorHere(head)) {
1211
+ baseExpr = typedLocal(head);
1212
+ baseRef = typedNodes.get(head);
1213
+ }
1214
+ else {
1215
+ if (restPath.length !== 0)
1216
+ return null;
1217
+ const scalar = inputScalarKind(comp.inputPorts?.[head]);
1218
+ if (scalar === undefined)
1219
+ return null;
1220
+ return { expr: `in.${goFieldName(head)}`, scalar };
1221
+ }
1222
+ let acc;
1223
+ let expr;
1224
+ try {
1225
+ const r = goTypedInternals.typedFieldAccess(baseExpr, baseRef, restPath, plan);
1226
+ expr = r.expr;
1227
+ acc = r.ref;
1228
+ }
1229
+ catch {
1230
+ return null;
1231
+ }
1232
+ if (acc.kind !== "scalar" || acc.scalar === "null")
1233
+ return null;
1234
+ return { expr, scalar: goScalar(acc.scalar) };
1235
+ };
1236
+ for (const pn of portNames) {
1237
+ const fieldGoType = portFieldGoType(m.ports[pn], comp.inputPorts, `map port '${pn}' of node '${node.id}'`);
1238
+ inits.push(emitPortInit(pn, m.ports[pn], fieldGoType, resolveRef));
1239
+ }
1240
+ out.push(`${il}ep_${s} := ${structName}{${inits.join(", ")}}`);
1241
+ return out;
1242
+ };
1243
+ const lines = [];
1244
+ const closers = [];
1245
+ lines.push(`\t// ── map '${node.id}' (${m.component}, ${batched ? "batched" : "per-element"}, into:${JSON.stringify(m.into ?? null)}${m.relationKind ? ", relationKind:" + m.relationKind : ""}${m.parent ? ", parent:" + m.parent : ""}) ──`);
1246
+ let indent = "\t";
1247
+ // the map is gated on the parent's produced flag (inline, like a relation child).
1248
+ if (m.parent !== undefined && priorNodeAt(m.parent, atPos)) {
1249
+ lines.push(`${indent}if produced_${sanitize(m.parent)} {`);
1250
+ closers.unshift(`${indent}}`);
1251
+ indent += "\t";
1252
+ }
1253
+ // over collection (typed field access to the parent node's arr — no serialize).
1254
+ lines.push(`${indent}over_${s} := ${overExpr}`);
1255
+ lines.push(`${indent}${typedLocal(node.id)} = make(${elemGo}, 0, len(over_${s}))`);
1256
+ const elemRowRef = mapElemRowRef(node, arrRef, plan);
1257
+ if (batched) {
1258
+ // BATCHED (#86 pt2 into / #93 shape 3 no-into): build all ports, dispatch ONCE. The concrete
1259
+ // Node_* returns RawRow_<comp>_<node>{IsError, Err, Rows []RawElem_} aligned to the over items.
1260
+ lines.push(`${indent}items_${s} := make([]${structName}, 0, len(over_${s}))`);
1261
+ lines.push(`${indent}for _, ${elemLocal(s)} := range over_${s} {`);
1262
+ for (const l of buildPorts(indent + "\t", elemLocal(s)))
1263
+ lines.push(l);
1264
+ lines.push(`${indent}\titems_${s} = append(items_${s}, ep_${s})`);
1265
+ lines.push(`${indent}}`);
1266
+ // empty over => empty result (handler not called), matching run_behavior.
1267
+ lines.push(`${indent}if len(items_${s}) > 0 {`);
1268
+ lines.push(`${indent}\tbp_${s} := ${batchStruct}{Items: items_${s}}`);
1269
+ lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(bp_${s}, nil)`);
1270
+ lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1271
+ 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}`);
1272
+ 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}`);
1273
+ lines.push(`${indent}\tfor mk_${s} := range over_${s} {`);
1274
+ if (hasInto) {
1275
+ // #86 pt2: zip-attach the into field onto each over element (augmented element copier).
1276
+ lines.push(`${indent}\t\tel_${s} := ${elemMat}(over_${s}[mk_${s}], mo_${s}.Rows[mk_${s}])`);
1277
+ }
1278
+ else {
1279
+ // #93 shape 3: collect each element DIRECTLY from the concrete per-element row (field copy).
1280
+ lines.push(`${indent}\t\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1281
+ lines.push(emitRowCopy(`el_${s}`, `mo_${s}.Rows[mk_${s}]`, elemRowRef, plan, indent.length + 2));
1282
+ }
1283
+ lines.push(`${indent}\t\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1284
+ lines.push(`${indent}\t}`);
1285
+ lines.push(`${indent}}`);
1286
+ }
1287
+ else {
1288
+ // #93 shape 2: NON-batched map...into — dispatch the child handler ONCE PER ELEMENT (N physical
1289
+ // requests, matching run_behavior's per-element loop). The concrete Node_* returns ONE RawElem_.
1290
+ lines.push(`${indent}for mk_${s} := range over_${s} {`);
1291
+ lines.push(`${indent}\t${elemLocal(s)} := over_${s}[mk_${s}]`);
1292
+ for (const l of buildPorts(indent + "\t", elemLocal(s)))
1293
+ lines.push(l);
1294
+ // per-element dispatch (ONE physical request per element). run_behavior passes the over element
1295
+ // as the bound value; the element data is fully conveyed by the native ports struct here, so bound
1296
+ // stays nil. Byte-equivalence holds because the bound value only feeds handler-internal logic.
1297
+ lines.push(`${indent}\tmo_${s}, mo_${s}Resolved := handlers.${nodeMethodName(comp.name, node.id)}(ep_${s}, nil)`);
1298
+ lines.push(`${indent}\tif !mo_${s}Resolved {`, `${indent}\t\treturn ${zeroOut}, ${goErr("UNKNOWN_COMPONENT", `"component '${m.component}' has no handler (fail-closed)"`)}`, `${indent}\t}`);
1299
+ 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}`);
1300
+ if (hasInto) {
1301
+ lines.push(`${indent}\tel_${s} := ${elemMat}(${elemLocal(s)}, mo_${s})`);
1302
+ }
1303
+ else {
1304
+ lines.push(`${indent}\tvar el_${s} ${renderTypeRef(arrRef.elem)}`);
1305
+ lines.push(emitRowCopy(`el_${s}`, `mo_${s}`, elemRowRef, plan, indent.length + 1));
1306
+ }
1307
+ lines.push(`${indent}\t${typedLocal(node.id)} = append(${typedLocal(node.id)}, el_${s})`);
1308
+ lines.push(`${indent}}`);
1309
+ }
1310
+ lines.push(`${indent}produced_${s} = true`);
1311
+ for (const c of closers)
1312
+ lines.push(c);
1313
+ void overElemGo;
1314
+ return lines.join("\n");
1315
+ }
1316
+ /** the map element loop variable (the over element, a typed struct). */
1317
+ function elemLocal(s) {
1318
+ return `oel_${s}`;
1319
+ }
1320
+ /**
1321
+ * emitProducedAwareValue — the #86 part 1 produced-aware output lowering (go). It mirrors the
1322
+ * boxed-typed emitTypedValue, but a `ref`/`refOpt` whose HEAD is a skippable relationSingle child is
1323
+ * gated on that node's `produced_<id>` flag: when the child is UNPRODUCED (bound parent field was
1324
+ * null/absent), run_behavior's writeUnproduced makes the node null, so a refOpt into it yields null
1325
+ * (the output opt field => nil). We emit that exactly: `if produced_<head> { <typed field access> }
1326
+ * else { <unproduced-rep for the expected type> }`. Obj/arr/concat/literal recurse; a ref into a
1327
+ * NON-skippable node delegates to the plain typed field access (identical to emitTypedValue).
1328
+ */
1329
+ function emitProducedAwareValue(node, expected, typedNodes, plan, skippable, skippableKind) {
1330
+ // ref / refOpt (classifyExpr encodes both as kind:"ref" with .opt).
1331
+ const e = classifyExpr(node);
1332
+ if (e.kind === "ref" && e.path.length >= 1 && typedNodes.has(e.path[0])) {
1333
+ const head = e.path[0];
1334
+ const baseRef = typedNodes.get(head);
1335
+ if (skippable.has(head)) {
1336
+ // produced-aware: gate the field access on produced_<head>. The unproduced branch yields the
1337
+ // node's unproduced rep per run_behavior's writeUnproduced (unproducedValue(relationKind)):
1338
+ // - single / absent → null (opt-nil);
1339
+ // - connection (#93 shape 1) → the empty connection {items:[],cursor:null}. Because a WHOLE
1340
+ // connection node has no field path (e.path.length===1), the unproduced rep is the struct
1341
+ // zero of the connection struct (its serializer normalizes a nil items slice to [] and the
1342
+ // nil cursor to null) — matching unproducedValue("connection") byte-for-byte.
1343
+ const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, e.path.slice(1), plan);
1344
+ const producedFlag = `produced_${sanitize(head)}`;
1345
+ const producedExpr = valueLeafTyped(expr, ref, e.opt, expected);
1346
+ const outTy = expected ? renderTypeRef(expected) : renderTypeRef(ref);
1347
+ const isConn = skippableKind.get(head) === "connection" && e.path.length === 1;
1348
+ // For a connection whole-node ref, the unproduced rep is the connection struct zero (empty
1349
+ // items + nil cursor), even if the output field is opt (refOpt): run_behavior always writes the
1350
+ // empty connection, never null. For single/field-access, the expected-type zero (nil for opt).
1351
+ const unproduced = isConn ? goConnectionUnproduced(ref, expected) : goZero(expected ?? ref);
1352
+ return {
1353
+ expr: `func() ${outTy} { if ${producedFlag} { return ${producedExpr} }; return ${unproduced} }()`,
1354
+ ref: expected ?? ref,
1355
+ };
1356
+ }
1357
+ // non-skippable node ref — identical to emitTypedValue's typed field access.
1358
+ const { expr, ref } = goTypedInternals.typedFieldAccess(typedLocal(head), baseRef, e.path.slice(1), plan);
1359
+ return { expr, ref };
1360
+ }
1361
+ // obj-assembly: recurse each field with the produced-aware lowering (the struct fields may each
1362
+ // ref a skippable node). Only handle the covered named-struct obj output.
1363
+ if (node !== null && typeof node === "object" && !Array.isArray(node)) {
1364
+ const keys = Object.keys(node);
1365
+ if (keys.length === 1 && keys[0] === "obj" && expected && expected.kind === "named") {
1366
+ const decl = findDecl(plan, expected.name);
1367
+ const fields = node.obj;
1368
+ const inits = decl.fields.map((f) => {
1369
+ const sub = emitProducedAwareValue(fields[f.name], f.type, typedNodes, plan, skippable, skippableKind);
1370
+ return `${goFieldName(f.name)}: ${sub.expr}`;
1371
+ });
1372
+ return { expr: `${expected.name}{${inits.join(", ")}}`, ref: expected };
1373
+ }
1374
+ }
1375
+ // everything else (literal / non-skippable ref / obj-of-non-named / arr / concat) — delegate to
1376
+ // the plain typed lowering (no skippable head reachable there in the covered corpus).
1377
+ return emitTypedValue(node, expected, typedNodes, plan);
1378
+ }
1379
+ /** Lower a typed field-access expr to the EXPECTED output type (used in the produced branch). When
1380
+ * the expected type is `opt` and the accessed value is already a *T pointer (the field itself is
1381
+ * opt), pass it through; when the accessed value is a bare leaf and expected is opt, take its addr. */
1382
+ function valueLeafTyped(expr, ref, opt, expected) {
1383
+ void opt;
1384
+ if (expected && expected.kind === "opt") {
1385
+ if (ref.kind === "opt")
1386
+ return expr; // *T already
1387
+ // wrap a bare value in a fresh pointer (matches renderTypeRef(opt)=*T).
1388
+ return `func() ${renderTypeRef(expected)} { __v := ${expr}; return &__v }()`;
1389
+ }
1390
+ return expr;
1391
+ }
1392
+ /**
1393
+ * goConnectionUnproduced — the unproduced rep for a SKIPPED relationKind:connection node (#93 shape
1394
+ * 1): the empty connection {items:[],cursor:null}. The connection outType is a named struct whose
1395
+ * serializer normalizes a nil items slice to [] and a nil cursor to null, so the struct ZERO
1396
+ * (`Tn{}`) serializes to exactly unproducedValue("connection"). When the output field is opt
1397
+ * (refOpt into the connection), run_behavior still writes the empty connection (never null), so we
1398
+ * wrap the struct zero in a fresh pointer rather than yielding nil.
1399
+ */
1400
+ function goConnectionUnproduced(nodeRef, expected) {
1401
+ const connRef = expected && expected.kind === "opt" ? expected.inner : (expected ?? nodeRef);
1402
+ const zero = goZero(connRef);
1403
+ if (expected && expected.kind === "opt") {
1404
+ return `func() ${renderTypeRef(expected)} { __c := ${zero}; return &__c }()`;
1405
+ }
1406
+ return zero;
1407
+ }
1408
+ /** Go zero value for a TypeRef (used as the error return of the struct runner). */
1409
+ function goZero(ref) {
1410
+ switch (ref.kind) {
1411
+ case "named":
1412
+ return `${ref.name}{}`;
1413
+ case "scalar":
1414
+ switch (ref.scalar) {
1415
+ case "string":
1416
+ return `""`;
1417
+ case "int":
1418
+ return "0";
1419
+ case "float":
1420
+ return "0";
1421
+ case "bool":
1422
+ return "false";
1423
+ case "null":
1424
+ return "nil";
1425
+ }
1426
+ return "nil";
1427
+ case "arr":
1428
+ return "nil";
1429
+ case "opt":
1430
+ return "nil";
1431
+ }
1432
+ }
1433
+ // bc#90 / runtime-free: the nv* helper set (nvVE/nvBindVE/nvFieldVE/nvCatVE/nvArrVE) is REMOVED —
1434
+ // those were dslcontracts.Value-based; the covered plane now lowers every port to a native Go value
1435
+ // (emitPortInit) so no VE helper is emitted, and the module imports ZERO bc runtime.
1436
+ // ── BindNativeRaw dispatch surface ──────────────────────────────────────────────────────
1437
+ /** The struct dispatch surface: ComponentNamesNativeRaw + the per-component struct runners
1438
+ * (run_native_raw_struct_<comp>) ARE the exposed API. The consumer calls the struct runner directly
1439
+ * (it returns the typed model — NO Value serialization on the read hot path, matching how graphddb
1440
+ * reads: a model, never a boxed Value). There is deliberately NO Value dispatch map — that would
1441
+ * require a Value serializer (the boxing residue bc#77 eliminates). A canonical Value observation
1442
+ * (equivalence pin) is produced by the SEPARATE test-only observe companion (goTypedNativeObserve),
1443
+ * which lives OUTSIDE this module so the covered module stays literal whole-file zero. */
1444
+ function emitStructSurface(native) {
1445
+ return `// ComponentNamesNativeRaw — covered reads exposed on the combined struct-native path (declaration
1446
+ // order). Each is driven via run_native_raw_struct_<comp>(handlers, input) -> (T, error): a STRUCT
1447
+ // return; the consumer keeps the model native (no Value serialization on the read hot path — the
1448
+ // boxing residue bc#77 removes). See INTEGRATION.md §6.
1449
+ var ComponentNamesNativeRaw = []string{${native.map((c) => JSON.stringify(c.name)).join(", ")}}`;
1450
+ }
1451
+ /** Minimal module header for a FULLY-covered module (bc#90 / runtime-free). A fully-covered module
1452
+ * imports ZERO bc runtime (only the std-lib packages it genuinely uses: `fmt` for a batched-map
1453
+ * mismatch Sprintf, `strings` for a concat builder, `sync` for a parallel stage). ExpectedSpecVersions
1454
+ * stays as a local `var` for provenance, but the old `dslcontracts.SpecVersions` skew `init()` is GONE
1455
+ * (change #5 — there is no runtime to skew against once the module is runtime-free). */
1456
+ function emitMinimalHeader(ctx, needStrings, needSync, needFmt) {
1457
+ const sv = ctx.specVersions;
1458
+ const imports = [];
1459
+ if (needFmt)
1460
+ imports.push(`\t"fmt"`);
1461
+ if (needStrings)
1462
+ imports.push(`\t"strings"`);
1463
+ if (needSync)
1464
+ imports.push(`\t"sync"`);
1465
+ const importBlock = imports.length > 0 ? `\nimport (\n${imports.join("\n")}\n)\n` : "";
1466
+ return `// GENERATED by behavior-contracts (bc#90, go-typed-native — the RUNTIME-FREE covered read de-box) — DO NOT EDIT.
1467
+ // Input: portable component-graph IR only. Every component here is a COVERED READ (sequential typed
1468
+ // componentRef); it is emitted ONLY as the combined native ports IN + raw struct OUT + inline
1469
+ // sequential runner below. There is NO generic Bind/dispatch/plan scaffolding AND NO bc-runtime import —
1470
+ // a fully-covered module is FULLY NATIVE (zero bc-runtime reference; only the std-lib it uses).
1471
+
1472
+ package behaviors
1473
+ ${importBlock}
1474
+ // ExpectedSpecVersions are the spec versions baked at generation time (provenance only — the module
1475
+ // is runtime-free, so there is no runtime SpecVersions to skew-check against, bc#90 / change #5).
1476
+ var ExpectedSpecVersions = map[string]int64{"behavior": ${sv.behavior}, "expression": ${sv.expression}, "plan": ${sv.plan}}`;
1477
+ }
1478
+ // ── module assembly ──────────────────────────────────────────────────────────────────
1479
+ function emit(ctx) {
1480
+ // Fail closed if a covered read SHAPE lacks the typed annotations (no silent re-box).
1481
+ for (const c of ctx.ir.components)
1482
+ assertTypedCoverage(c);
1483
+ const native = ctx.ir.components.filter(isNativeRaw);
1484
+ const nonNative = ctx.ir.components.filter((c) => !isNativeRaw(c));
1485
+ const plan = buildTypePlan(ctx.ir);
1486
+ const decls = emitTypeDecls(plan);
1487
+ // Emit the runners first so the nv* helper usage flags are known.
1488
+ const flags = { bind: false, field: false, cat: false, arr: false };
1489
+ const structs = [];
1490
+ const inStructs = [];
1491
+ const rowStructs = [];
1492
+ const handlerIfaces = [];
1493
+ const elemCopiers = [];
1494
+ const runnerCodes = [];
1495
+ for (const c of native) {
1496
+ inStructs.push(emitInStruct(c));
1497
+ const typedNodes = new Map();
1498
+ for (const n of c.body) {
1499
+ if ("map" in n) {
1500
+ // #86 pt2: a map node emits an element ports struct + a batch struct (Items []Value of the
1501
+ // per-element native ports structs; no key-value object in the accessor).
1502
+ structs.push(emitMapPortsStructs(c, n));
1503
+ }
1504
+ else {
1505
+ structs.push(emitPortsStruct(c, n));
1506
+ }
1507
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1508
+ }
1509
+ // CONCRETE per-node row structs + the per-component handler interface (the fully-concrete seam).
1510
+ rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
1511
+ handlerIfaces.push(emitHandlerInterface(c, typedNodes, plan));
1512
+ // augmented-element copiers for covered map...into nodes (over fields + concrete into row).
1513
+ const mm = emitMapElemMaterializers(c, typedNodes, plan);
1514
+ if (mm)
1515
+ elemCopiers.push(mm);
1516
+ const r = emitNativeRawRunner(c, plan, flags);
1517
+ runnerCodes.push(r.code);
1518
+ }
1519
+ const structSurface = emitStructSurface(native);
1520
+ const behaviorErrorType = emitBehaviorErrorType();
1521
+ // bc#87: a covered component with a real-concurrency stage emits sync.WaitGroup + a semaphore.
1522
+ const needSync = native.some((c) => concurrencyPlan(c) !== null);
1523
+ // bc#90: the ONLY std-lib `fmt.Sprintf` site on the covered plane is the batched-map result-count
1524
+ // mismatch error — so `fmt` is imported iff a covered component has a batched map node.
1525
+ const needFmt = native.some((c) => c.body.some((n) => "map" in n && n.map.batched === true));
1526
+ // The FULLY-covered module (no non-native components) gets a MINIMAL, RUNTIME-FREE header — NO
1527
+ // bc-runtime import at all (bc#90).
1528
+ // FAIL CLOSED (bc#86/#89): the --typed-native (native codegen) surface is native-only. If ANY
1529
+ // component is not a covered native read, we do NOT silently box it through the straight-line
1530
+ // emitter (the old concealment) — we throw a LOUD GeneratorFailure naming every uncovered component
1531
+ // and its specific reject reason. A fully-covered module (nonNative.length === 0) is unchanged: the
1532
+ // minimal, runtime-free header.
1533
+ if (nonNative.length > 0) {
1534
+ const rejects = nonNative
1535
+ .map((c) => ` - component '${c.name}': ${coverageRejectReason(c) ?? "not a covered native read"}`)
1536
+ .join("\n");
1537
+ 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` +
1538
+ `The --typed-native (native codegen) surface is native-only and fail-closed: it does not silently ` +
1539
+ `box uncovered shapes. Implement native coverage for this shape, fix the spec if the shape is ` +
1540
+ `genuinely runtime-dynamic, or generate this component on the --ir dynamic interpreter surface.`);
1541
+ }
1542
+ const header = emitMinimalHeader(ctx, false, needSync, needFmt);
1543
+ const banner = `// COMBINED READ layer (bc#90 — the RUNTIME-FREE read de-box; STRUCT-ONLY, FULLY-NATIVE surface). A
1544
+ // covered read is a strictly-sequential typed componentRef chain (point reads + relationKind:single
1545
+ // children), emitted ONLY here: each node builds a native ports struct by direct field assignment (every
1546
+ // port is a CONCRETE Go value — string / bool / []string projection / int64 limit — NO boxed Value, NO
1547
+ // PortReader accessor); the CONCRETE per-component handler interface (Handler_<comp>) takes a typed
1548
+ // produced-aware 'bound' pointer and returns a CONCRETE per-node row struct (Row_<comp>_<node>, typed
1549
+ // fields = the node's outType) whose fields the runner reads DIRECTLY and copies into the node's outType
1550
+ // struct. The covered plane carries NO bc-runtime reference at all — failures use the LOCAL
1551
+ // *BehaviorError (same codes, byte-equal to run_behavior). Exec is INLINE sequential (no plan driver) so
1552
+ // a relation child reads the parent's REAL struct result via direct field access and the child-present
1553
+ // decision is made from the real parent value — relationSingle / connection converge with run_behavior
1554
+ // (fixes #323/#74). The exposed API is the STRUCT-returning run_native_raw_struct_<comp> (the consumer
1555
+ // keeps the model native — no serialization on the read hot path). This module is FULLY NATIVE: a naive
1556
+ // grep for boxing primitives OR for the bc-runtime package finds nothing, by design (bc#90/runtime-free).`;
1557
+ const sections = [
1558
+ banner,
1559
+ `// Local concrete failure type (runtime-free) — a covered runner returns *BehaviorError (satisfies\n// error) instead of a bc-runtime failure, so the fully-covered module imports ZERO bc runtime.\n${behaviorErrorType}`,
1560
+ decls ? `// Typed struct declarations (outType-derived; hash-dedup — shared type plan).\n${decls}` : undefined,
1561
+ `// Per-component CONCRETE input structs (fields = inputPorts; the consumer builds them natively —\n// no generic *Obj crosses the covered read boundary).\n${inStructs.join("\n\n")}`,
1562
+ `// CONCRETE per-node handler-result row structs (typed fields = the node's outType; + error signal).\n${rowStructs.join("\n\n")}`,
1563
+ `// Per-component CONCRETE handler interfaces (one typed Node_* method per covered node).\n${handlerIfaces.join("\n\n")}`,
1564
+ elemCopiers.length
1565
+ ? `// Augmented map-element copiers (over element fields + the concrete into row — pure assignment).\n${elemCopiers.join("\n\n")}`
1566
+ : undefined,
1567
+ `// Native ports structs (one per componentRef node; typed per the static port type).\n${structs.join("\n\n")}`,
1568
+ `// Combined read runners (STRUCT-returning — the fully de-plumbed path).\n${runnerCodes.join("\n\n")}`,
1569
+ structSurface,
1570
+ ].filter((s) => !!s);
1571
+ return `${header}\n\n${sections.join("\n\n")}\n`;
1572
+ }
1573
+ /**
1574
+ * goTypedNativeObserve — the TEST-ONLY observe companion (a SEPARATE file in the SAME package as the
1575
+ * covered module). It carries the typed->Value serializers (ser_*) + an ObserveNativeRaw dispatch
1576
+ * that drives the module-private struct runner and serializes its result to a canonical Value for
1577
+ * the equivalence pin. This lives OUTSIDE the covered module so the module stays literal whole-file
1578
+ * zero (the ser_* NewObj is test glue, never on the consumer's read hot path). Emitted only for the
1579
+ * fully-covered case (the equivalence harness); production consumers never use it.
1580
+ */
1581
+ export function goTypedNativeObserve(ir, runtimeImport) {
1582
+ const components = ir.components;
1583
+ const native = components.filter(isNativeRaw);
1584
+ const plan = buildTypePlan(ir);
1585
+ const serializers = emitSerializers(plan);
1586
+ const marshallers = goTypedInternals.emitMarshallers(plan);
1587
+ const mustHelpers = emitMustHelpers(plan);
1588
+ // arr/opt node materialize helpers (mustArr_*/mustOpt_*) for the concrete-row decode of a node whose
1589
+ // whole outType is a bare arr/opt (materializeExpr emits mustArr_/mustOpt_ for those).
1590
+ const arrOptHelpers = goTypedInternals.emitArrOptNodeHelpers({ ir }, plan);
1591
+ const arms = native
1592
+ .map((c) => {
1593
+ const outRef = c.outputType !== undefined ? deriveTypeRef(c.outputType, plan) : undefined;
1594
+ const ser = outRef ? serializeTyped("tv", outRef, plan) : "tv";
1595
+ // decode the generic *Obj input into the CONCRETE In_<comp> struct (test glue — ONE decode at the
1596
+ // top, OUTSIDE the covered hot path; a REQUIRED declared input is present so an absent key is a
1597
+ // fail-closed decode error surfaced here, never on the native runner path).
1598
+ return `\tif name == ${JSON.stringify(c.name)} {\n\t\tin, ierr := decode_${inStructName(c.name)}(input)\n\t\tif ierr != nil {\n\t\t\treturn nil, ierr\n\t\t}\n\t\ttv, err := run_native_raw_struct_${sanitize(c.name)}(handlers, in)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn ${ser}, nil\n\t}`;
1599
+ })
1600
+ .join("\n");
1601
+ // input decoders (TEST glue): generic *Obj -> concrete In_<comp>. Scalar fields read the typed value
1602
+ // (a `.(T)` here is on the SDK/wire Value in the OBSERVE companion, never in the covered module).
1603
+ const inDecoders = native.map((c) => emitInDecoder(c)).join("\n\n");
1604
+ // The observe companion drives EVERY covered component, so its handler type param H must satisfy
1605
+ // EVERY per-component concrete Handler_<comp> interface — a combined constraint embeds them all.
1606
+ const combinedConstraint = `allNativeRawHandlers`;
1607
+ const constraintDecl = `// ${combinedConstraint} — the combined constraint embedding every covered component's concrete
1608
+ // handler interface, so one handler value drives all covered runners (test glue only).
1609
+ type ${combinedConstraint} interface {
1610
+ ${native.map((c) => `\t${handlerIfaceName(c.name)}`).join("\n")}
1611
+ }`;
1612
+ // ── concrete scripted adapter (TEST glue): implements every Node_* method by draining a per-
1613
+ // COMPONENT scripted-outcome queue and DECODING the scripted ok Value into the CONCRETE row struct.
1614
+ // This is where the generic scripted source (a Value queue) meets the concrete per-node seam: the
1615
+ // decode is generated (it knows the concrete row type), so the runner stays fully concrete. ──
1616
+ const decodeFns = [];
1617
+ const methodImpls = [];
1618
+ const emittedDecode = new Set();
1619
+ for (const c of native) {
1620
+ const typedNodes = new Map();
1621
+ for (const n of c.body)
1622
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1623
+ for (const n of c.body) {
1624
+ const ref = typedNodes.get(n.id);
1625
+ if ("map" in n) {
1626
+ const m = n.map;
1627
+ const batched = m.batched === true;
1628
+ const arrRef = ref;
1629
+ const elemRowRef = mapElemRowRef(n, arrRef, plan);
1630
+ const elemT = rawElemStructName(c.name, n.id);
1631
+ const batchT = rawRowStructName(c.name, n.id);
1632
+ const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
1633
+ const portsT = batched ? `${portsStructName(c.name, n.id)}_batch` : portsStructName(c.name, n.id);
1634
+ const retT = batched ? batchT : elemT;
1635
+ const mapBoundT = boundGoType(n, c, typedNodes, plan);
1636
+ if (batched) {
1637
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${batchT}, bool) {
1638
+ src, ok := s.next(${JSON.stringify(m.component)})
1639
+ if !ok {
1640
+ return ${batchT}{}, false
1641
+ }
1642
+ if src.isErr {
1643
+ return ${batchT}{IsError: true, Err: src.err}, true
1644
+ }
1645
+ arr, _ := src.ok.([]${PKG}.Value)
1646
+ rows := make([]${elemT}, 0, len(arr))
1647
+ for _, ev := range arr {
1648
+ rows = append(rows, ${elemDecode}(ev))
1649
+ }
1650
+ return ${batchT}{Rows: rows}, true
1651
+ }`);
1652
+ }
1653
+ else {
1654
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, n.id)}(_ ${portsT}, _ ${mapBoundT}) (${elemT}, bool) {
1655
+ src, ok := s.next(${JSON.stringify(m.component)})
1656
+ if !ok {
1657
+ return ${elemT}{}, false
1658
+ }
1659
+ if src.isErr {
1660
+ return ${elemT}{IsError: true, Err: src.err}, true
1661
+ }
1662
+ return ${elemDecode}(src.ok), true
1663
+ }`);
1664
+ }
1665
+ void retT;
1666
+ continue;
1667
+ }
1668
+ const node = n;
1669
+ const rowT = rawRowStructName(c.name, node.id);
1670
+ const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
1671
+ const nodeBoundT = boundGoType(node, c, typedNodes, plan);
1672
+ methodImpls.push(`func (s *ScriptedNativeRaw) ${nodeMethodName(c.name, node.id)}(_ ${portsStructName(c.name, node.id)}, _ ${nodeBoundT}) (${rowT}, bool) {
1673
+ src, ok := s.next(${JSON.stringify(node.component)})
1674
+ if !ok {
1675
+ return ${rowT}{}, false
1676
+ }
1677
+ if src.isErr {
1678
+ return ${rowT}{IsError: true, Err: src.err}, true
1679
+ }
1680
+ return ${decode}(src.ok), true
1681
+ }`);
1682
+ }
1683
+ }
1684
+ const adapter = `// scriptSrc — a scripted per-component outcome (an ok Value or an error), drained in order.
1685
+ type scriptSrc struct {
1686
+ isErr bool
1687
+ err string
1688
+ ok ${PKG}.Value
1689
+ }
1690
+
1691
+ // ScriptedNativeRaw — the CONCRETE test handler: it satisfies every Handler_<comp> interface by
1692
+ // draining a per-COMPONENT scripted queue and decoding the ok Value into the concrete row struct.
1693
+ // The decode (decodeRow_*) is generated, so the covered runner drives fully-concrete Node_* methods.
1694
+ // EXPORTED so the runner glue (a separate package) can name it for the generic instantiation.
1695
+ type ScriptedNativeRaw struct {
1696
+ mu sync.Mutex
1697
+ queues map[string][]scriptSrc
1698
+ seen int
1699
+ }
1700
+
1701
+ // NewScriptedNativeRaw builds the concrete test handler from the JSON handlers spec (the SAME shape
1702
+ // the reference scriptedHandlers reads): { "<component>": {ok|error} | [ {ok|error}, … ] }. Each
1703
+ // component's ok Value is DecodeValue'd once here (test glue); the per-node method decodes it into the
1704
+ // concrete row. Returns the handler + any decode error (surfaced by the runner as a fatal).
1705
+ func NewScriptedNativeRaw(spec *${PKG}.JObj) (*ScriptedNativeRaw, error) {
1706
+ queues := map[string][]scriptSrc{}
1707
+ if spec != nil {
1708
+ for _, comp := range spec.Keys {
1709
+ var entries []${PKG}.JNode
1710
+ switch t := spec.Vals[comp].(type) {
1711
+ case []${PKG}.JNode:
1712
+ entries = t
1713
+ default:
1714
+ entries = []${PKG}.JNode{t}
1715
+ }
1716
+ for _, e := range entries {
1717
+ eo, _ := e.(*${PKG}.JObj)
1718
+ if okNode, has := eo.Get("ok"); has {
1719
+ val, derr := ${PKG}.DecodeValue(okNode)
1720
+ if derr != nil {
1721
+ return nil, derr
1722
+ }
1723
+ queues[comp] = append(queues[comp], scriptSrc{ok: val})
1724
+ continue
1725
+ }
1726
+ ev, _ := eo.Get("error")
1727
+ es, _ := ev.(string)
1728
+ queues[comp] = append(queues[comp], scriptSrc{isErr: true, err: es})
1729
+ }
1730
+ }
1731
+ }
1732
+ return &ScriptedNativeRaw{queues: queues}, nil
1733
+ }
1734
+
1735
+ // next drains one scripted outcome for a component (last entry repeats — mirrors the reference
1736
+ // scriptedHandlers), returns ok=false when the component has no scripted queue (fail-closed). The
1737
+ // bc#87 parallel-stage runner calls Node_* from multiple goroutines, so the drain is mutex-guarded.
1738
+ func (s *ScriptedNativeRaw) next(component string) (scriptSrc, bool) {
1739
+ s.mu.Lock()
1740
+ defer s.mu.Unlock()
1741
+ s.seen++
1742
+ q, ok := s.queues[component]
1743
+ if !ok || len(q) == 0 {
1744
+ return scriptSrc{}, false
1745
+ }
1746
+ if len(q) > 1 {
1747
+ v := q[0]
1748
+ s.queues[component] = q[1:]
1749
+ return v, true
1750
+ }
1751
+ return q[0], true
1752
+ }`;
1753
+ return `// GENERATED test-only observe companion (bc#77) — NOT part of the covered module. Serializes the
1754
+ // struct-native runner's result to a canonical Value for the equivalence pin, and carries the CONCRETE
1755
+ // scripted test handler (scriptedNativeRaw) that satisfies every Handler_<comp> interface by decoding a
1756
+ // scripted Value into the concrete per-node row. The consumer never uses this (it keeps the model
1757
+ // native + implements Handler_<comp> over its own wire payload); it exists only so the test harness can
1758
+ // byte-compare the STRUCT-only covered read against run_behavior. Same package as the covered module.
1759
+ package behaviors
1760
+
1761
+ import (
1762
+ "sync"
1763
+
1764
+ ${PKG} "${runtimeImport ?? "github.com/foo-ogawa/behavior-contracts/go"}"
1765
+ )
1766
+
1767
+ // typed -> Value serializers (TEST glue only — the covered module has none).
1768
+ ${serializers}
1769
+
1770
+ // Value -> typed struct marshallers (+ must* helpers) for the concrete-row decode (TEST glue only).
1771
+ ${marshallers}
1772
+
1773
+ ${mustHelpers}
1774
+
1775
+ ${arrOptHelpers}
1776
+
1777
+ // per-node concrete-row decoders (scripted Value -> RawRow_/RawElem_ struct).
1778
+ ${decodeFns.join("\n\n")}
1779
+
1780
+ // input decoders (generic *Obj -> concrete In_<comp>; TEST glue, one decode at the observe boundary).
1781
+ ${inDecoders}
1782
+
1783
+ ${adapter}
1784
+
1785
+ ${constraintDecl}
1786
+
1787
+ // scriptedNativeRaw method impls (one per covered node — the concrete Node_* seam, test glue).
1788
+ ${methodImpls.join("\n\n")}
1789
+
1790
+ // ObserveNativeRaw drives the struct-native runner and serializes to a canonical Value (test only).
1791
+ // GENERIC over the CONCRETE combined handler type H — so each enclosed run_native_raw_struct_* call is
1792
+ // instantiated at the concrete handler type (direct, devirtualized per-node method calls), not at an
1793
+ // interface. Instantiate with your concrete handler type to preserve the covered path's concreteness.
1794
+ func ObserveNativeRaw[H ${combinedConstraint}](name string, handlers H, input *${PKG}.Obj) (${PKG}.Value, error) {
1795
+ ${arms}
1796
+ return nil, ${PKG}.NewBehaviorFailure("UNKNOWN_ENTRY", "component '"+name+"' is not a covered read (fail-closed)")
1797
+ }
1798
+ `;
1799
+ }
1800
+ /** emitDecodeRow — emit (once, deduped) a decoder `decodeRow_<comp>_<node><suffix>(v Value) <rowT>`
1801
+ * that materializes the scripted ok Value into the concrete row struct (typed fields = the outType /
1802
+ * elem-row type). Named: reuse the Value->struct marshaller + copy fields; scalar/arr/opt: materialize
1803
+ * into .Val. TEST glue only (the production consumer decodes its own wire payload). */
1804
+ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix) {
1805
+ const fn = `decodeRow_${sanitize(compName)}_${nodeId.replace(/[^A-Za-z0-9_]/g, "_")}${suffix}`;
1806
+ if (emitted.has(fn))
1807
+ return fn;
1808
+ emitted.add(fn);
1809
+ const lines = [];
1810
+ lines.push(`func ${fn}(v ${PKG}.Value) ${rowT} {`);
1811
+ lines.push(`\tvar out ${rowT}`);
1812
+ if (ref.kind === "named") {
1813
+ const decl = findDecl(plan, ref.name);
1814
+ lines.push(`\tt := ${goTypedInternals.materializeExpr("v", ref, plan)}`);
1815
+ for (const f of decl.fields) {
1816
+ lines.push(`\tout.${goFieldName(f.name)} = t.${goFieldName(f.name)}`);
1817
+ }
1818
+ }
1819
+ else {
1820
+ lines.push(`\tout.Val = ${goTypedInternals.materializeExpr("v", ref, plan)}`);
1821
+ }
1822
+ lines.push(`\treturn out`);
1823
+ lines.push(`}`);
1824
+ out.push(lines.join("\n"));
1825
+ return fn;
1826
+ }
1827
+ /**
1828
+ * goTypedNativeEmitter — language id `go-typed-native` (bc#77). Emits the generic straight-line
1829
+ * body (for non-covered components / helpers) plus the COMBINED READ layer (native ports struct
1830
+ * IN + raw struct result OUT + inline sequential exec) and the BindNativeRaw surface. Requires
1831
+ * outType/outputType on every covered read (fail-closed otherwise).
1832
+ */
1833
+ export const goTypedNativeEmitter = {
1834
+ language: "go-typed-native",
1835
+ fileExtension: ".go",
1836
+ defaultRuntimeImport: "github.com/foo-ogawa/behavior-contracts/go/coderuntime",
1837
+ filenameHint: "behaviors.typed.native.generated.go",
1838
+ emit,
1839
+ };
1840
+ //# sourceMappingURL=emit-typed-native-go.js.map