behavior-contracts 0.7.3 → 0.8.1

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 (63) hide show
  1. package/README.md +0 -1
  2. package/dist/authoring.d.ts +215 -3
  3. package/dist/authoring.d.ts.map +1 -1
  4. package/dist/authoring.js +269 -8
  5. package/dist/authoring.js.map +1 -1
  6. package/dist/behavior.d.ts +33 -1
  7. package/dist/behavior.d.ts.map +1 -1
  8. package/dist/behavior.js +49 -1
  9. package/dist/behavior.js.map +1 -1
  10. package/dist/bind.d.ts +64 -0
  11. package/dist/bind.d.ts.map +1 -0
  12. package/dist/bind.js +131 -0
  13. package/dist/bind.js.map +1 -0
  14. package/dist/builder.d.ts +30 -88
  15. package/dist/builder.d.ts.map +1 -1
  16. package/dist/builder.js +48 -56
  17. package/dist/builder.js.map +1 -1
  18. package/dist/expr-operator-types.d.ts +89 -0
  19. package/dist/expr-operator-types.d.ts.map +1 -0
  20. package/dist/expr-operator-types.js +134 -0
  21. package/dist/expr-operator-types.js.map +1 -0
  22. package/dist/generator/async-plan.d.ts.map +1 -1
  23. package/dist/generator/async-plan.js +4 -0
  24. package/dist/generator/async-plan.js.map +1 -1
  25. package/dist/generator/core.d.ts +15 -0
  26. package/dist/generator/core.d.ts.map +1 -1
  27. package/dist/generator/core.js +2 -0
  28. package/dist/generator/core.js.map +1 -1
  29. package/dist/generator/emit-php.d.ts.map +1 -1
  30. package/dist/generator/emit-php.js +1 -0
  31. package/dist/generator/emit-php.js.map +1 -1
  32. package/dist/generator/emit-python.d.ts.map +1 -1
  33. package/dist/generator/emit-python.js +1 -0
  34. package/dist/generator/emit-python.js.map +1 -1
  35. package/dist/generator/emit-straightline-typed-typescript.d.ts.map +1 -1
  36. package/dist/generator/emit-straightline-typed-typescript.js +1 -0
  37. package/dist/generator/emit-straightline-typed-typescript.js.map +1 -1
  38. package/dist/generator/emit-typed-native-go.d.ts.map +1 -1
  39. package/dist/generator/emit-typed-native-go.js +163 -26
  40. package/dist/generator/emit-typed-native-go.js.map +1 -1
  41. package/dist/generator/emit-typed-native-rust.d.ts.map +1 -1
  42. package/dist/generator/emit-typed-native-rust.js +152 -32
  43. package/dist/generator/emit-typed-native-rust.js.map +1 -1
  44. package/dist/generator/emit-typescript.d.ts.map +1 -1
  45. package/dist/generator/emit-typescript.js +24 -8
  46. package/dist/generator/emit-typescript.js.map +1 -1
  47. package/dist/generator/index.d.ts +1 -1
  48. package/dist/generator/index.d.ts.map +1 -1
  49. package/dist/generator/index.js +7 -5
  50. package/dist/generator/index.js.map +1 -1
  51. package/dist/index.d.ts +41 -8
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +27 -6
  54. package/dist/index.js.map +1 -1
  55. package/dist/provenance.d.ts +102 -0
  56. package/dist/provenance.d.ts.map +1 -0
  57. package/dist/provenance.js +128 -0
  58. package/dist/provenance.js.map +1 -0
  59. package/dist/type-gate.d.ts +63 -0
  60. package/dist/type-gate.d.ts.map +1 -0
  61. package/dist/type-gate.js +313 -0
  62. package/dist/type-gate.js.map +1 -0
  63. package/package.json +4 -3
@@ -1,4 +1,5 @@
1
1
  import { GeneratorFailure } from "./core.js";
2
+ import { isControlGate } from "../behavior.js";
2
3
  import { rustStraightlineInternals } from "./emit-shared-rust.js";
3
4
  import { rustTypedInternals } from "./emit-shared-rust-typed.js";
4
5
  import { buildTypePlan, deriveTypeRef, findDecl } from "./typed.js";
@@ -178,13 +179,17 @@ function staticStringArrElems(node) {
178
179
  * Value-typed input) FAILS CLOSED — the covered read plane admits NO boxed escape (there is no `Value`
179
180
  * fallback on the native module). `where` names the emission site for the error.
180
181
  */
181
- function portFieldRustType(node, inputPorts, where = "port", asBinding, plan) {
182
+ function portFieldRustType(node, inputPorts, where = "port", asBinding, plan, typedNodes) {
182
183
  if (staticStringArrElems(node) !== null)
183
184
  return "Vec<&'static str>";
184
185
  // #110: a componentRef port bound to a runtime array input port (with a resolvable elemType) → Vec<ElemT>.
185
186
  const arrElem = portArrayElemRef(node, inputPorts, plan);
186
187
  if (arrElem !== null)
187
188
  return `Vec<${renderTypeRef(arrElem)}>`;
189
+ // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) → Vec<ElemT>.
190
+ const priorArr = priorNodeArrElemInfo(node, typedNodes, plan);
191
+ if (priorArr !== null)
192
+ return `Vec<${renderTypeRef(priorArr.elemRef)}>`;
188
193
  const num = numLiteralRustExpr(node);
189
194
  if (num !== null)
190
195
  return typeof node === "number" && Number.isInteger(node) ? "i64" : "f64";
@@ -201,13 +206,13 @@ function portFieldRustType(node, inputPorts, where = "port", asBinding, plan) {
201
206
  * reads `ports.f_<field>` DIRECTLY. The struct derives Clone (a bc#87 parallel stage clones it into the
202
207
  * per-worker dispatch). Every field is a NATIVE Rust type (bc#90 / runtime-free) — NO boxed `Value`.
203
208
  */
204
- function emitPortsStruct(comp, node, asBinding, plan) {
209
+ function emitPortsStruct(comp, node, asBinding, plan, typedNodes) {
205
210
  const name = portsStructName(comp.name, node.id);
206
211
  const portNames = Object.keys(node.ports);
207
212
  if (portNames.length === 0) {
208
213
  return `// ${name} — native ports for node '${node.id}' (${node.component}); no ports.\n#[derive(Clone)]\npub struct ${name};`;
209
214
  }
210
- const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan));
215
+ const types = portNames.map((p) => portFieldRustType(node.ports[p], comp.inputPorts, `port '${p}' of node '${node.id}'`, asBinding, plan, typedNodes));
211
216
  const fields = portNames
212
217
  .map((p, i) => ` pub ${portFieldName(p)}: ${types[i]}, // ${JSON.stringify(p)}`)
213
218
  .join("\n");
@@ -681,6 +686,44 @@ function mapOverElemInfo(comp, node, typedNodes, plan) {
681
686
  function mapOverElemType(comp, node, typedNodes, plan) {
682
687
  return renderTypeRef(mapOverElemInfo(comp, node, typedNodes, plan).elemRef);
683
688
  }
689
+ /**
690
+ * priorNodeArrElemInfo (#129, rust twin) — resolve a leaf port that is a `ref` into a PRIOR BODY NODE
691
+ * whose annotated `outType` is (or reaches, through a field path) a native array, to its element TypeRef
692
+ * + the OWNED Rust Vec expression that yields it. GENERALIZES the `map…over` prior-node-arr dataflow
693
+ * (mapOverElemInfo's first branch) to ANY leaf input port: a fold/dedup leaf whose `items` port is
694
+ * `{ref:["<priorNode>"]}` (or `.items` of a prior connection node) now lowers to `Vec<ElemT>` cloned
695
+ * out of the prior node's typed cell — ZERO boxed Value on the read hot path.
696
+ *
697
+ * The element type comes STRICTLY from the prior node's EXPLICIT `outType` annotation walked through
698
+ * typedFieldAccess (BC does NOT infer — consumer-interface C3 / [[graphddb-typed-native-static-link]]):
699
+ * if the head is not a prior body node, or its outType (through the field path) is not an `arr`, this
700
+ * returns null and the caller keeps its existing fail-closed behaviour (never a silent box). A
701
+ * single-segment INPUT-array port (elemType) is a DIFFERENT covered shape (portArrayElemRef) — handled
702
+ * ahead of this. Requires the typedNodes map (prior-node outType TypeRefs) and a TypePlan.
703
+ */
704
+ function priorNodeArrElemInfo(node, typedNodes, plan) {
705
+ if (typedNodes === undefined || plan === undefined)
706
+ return null;
707
+ if (staticArrElems(node) !== null)
708
+ return null; // a static array literal — not a prior-node ref.
709
+ const e = classifyExpr(node);
710
+ if (e.kind !== "ref" || e.opt || e.path.length < 1)
711
+ return null;
712
+ if (!typedNodes.has(e.path[0]))
713
+ return null; // head is not a prior body node → not this shape.
714
+ const baseRef = typedNodes.get(e.path[0]);
715
+ let acc;
716
+ try {
717
+ acc = typedFieldAccess(`${typedCell(e.path[0])}.borrow()`, baseRef, e.path.slice(1), plan);
718
+ }
719
+ catch {
720
+ return null; // the field path does not resolve on the prior node's outType → fail-closed at caller.
721
+ }
722
+ if (acc.ref.kind !== "arr")
723
+ return null; // the prior-node result (through the path) is not an array.
724
+ // OWNED: clone the borrowed slice out of the RefCell so the ports struct owns its Vec<ElemT>.
725
+ return { elemRef: acc.ref.elem, overExpr: `${acc.expr}.clone()` };
726
+ }
684
727
  // ── eligibility ─────────────────────────────────────────────────────────────────────
685
728
  function reconstructR(e) {
686
729
  switch (e.kind) {
@@ -884,14 +927,27 @@ function coverageRejectReason(comp) {
884
927
  // shape). Kept fail-closed: a non-componentRef member is rejected.
885
928
  for (const [i] of ep.parallelStageOf) {
886
929
  const n = comp.body[i];
930
+ // #125: a when(...) control-gate cond IS allowed as a parallel-stage member — it is a pure
931
+ // Expression (no dispatch), settled in ascending preflight order like the bindField key gate, so its
932
+ // produced_<gate> flag is决定済み before the stage's gated children dispatch. No handler, no boxed
933
+ // result — it does not disturb the bounded-concurrency error-precedence protocol.
934
+ if (isControlGate(n))
935
+ continue;
887
936
  if (!("component" in n))
888
937
  return `node '${n.id}' is a non-componentRef member of a parallel stage (not native-covered)`;
889
938
  }
890
939
  const bodyIds = new Set(comp.body.map((b) => b.id));
891
940
  for (const n of comp.body) {
892
941
  if ("cond" in n) {
893
- // #108: a cond node is native-covered (its if/then/else compile via the runtime-free
894
- // NativeExprCompiler; an uncovered expr fails closed at emit). cond requires outType.
942
+ // #125: a when(...) control-gate cond (`{if, then:{obj:{ok:true}}, else:null}`) は制御スキップ
943
+ // マーカーで出力データ型を持たない(A3 typeless design)— outType を要求しない。gate `if`
944
+ // native bool へコンパイルし produced_<gate> に写すだけで、分岐内ノードは既存の
945
+ // `parent=gate, bindField:"ok"` skip-gate(run_behavior の null-binding skip)が実行選択する。
946
+ if (isControlGate(n))
947
+ continue;
948
+ // #108: a DATA-JOIN cond node (Φ 合流) is native-covered (its if/then/else compile via the runtime-
949
+ // free NativeExprCompiler; an uncovered expr fails closed at emit). A data-join cond requires outType
950
+ // (合流値を de-box する対象だから — control gate だけがこの要求を免除される)。
895
951
  if (n.outType === undefined)
896
952
  return `node '${n.id}' cond has no outType (required to de-box the branch join)`;
897
953
  continue;
@@ -911,7 +967,12 @@ function coverageRejectReason(comp) {
911
967
  const cr = n;
912
968
  if (cr.policy !== undefined && cr.policy !== "fail")
913
969
  return `node '${n.id}' componentRef policy '${cr.policy}' (produced-aware output not yet native-covered)`;
914
- if (cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
970
+ // #125: a when(...) control-gate child (`parent=gate, bindField:"ok"`). Its bindField is the gate's
971
+ // skip signal, NOT a relation data key — so the relationKind constraint below (which governs a
972
+ // relation bindField into a single|connection parent) does not apply. Its skip is driven by the
973
+ // gate's produced_<gate> flag; the child itself can be a plain point read (or a relation read).
974
+ const childOfGate = cr.parent !== undefined && isControlGate(comp.body.find((b) => b.id === cr.parent));
975
+ if (!childOfGate && cr.bindField !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
915
976
  return `node '${n.id}' relationKind '${cr.relationKind ?? "point"}' not native-covered (bindField only supports single|connection)`;
916
977
  if (cr.relationKind !== undefined && cr.relationKind !== "single" && cr.relationKind !== "connection")
917
978
  return `relationKind '${cr.relationKind}' not native-covered (node '${n.id}')`;
@@ -926,8 +987,10 @@ function coverageRejectReason(comp) {
926
987
  function isFullyTyped(comp) {
927
988
  if (comp.outputType === undefined)
928
989
  return false;
990
+ // #125: a control-gate cond is a typeless skip marker (A3) — it never de-boxes a result, so it is
991
+ // exempt from the "every node carries outType" de-box requirement (matches type-gate's exemption).
929
992
  for (const n of comp.body)
930
- if (n.outType === undefined)
993
+ if (!isControlGate(n) && n.outType === undefined)
931
994
  return false;
932
995
  return true;
933
996
  }
@@ -1069,7 +1132,7 @@ function emitNativeScalar(node, want, resolveRef) {
1069
1132
  * a boxed escape would re-introduce the bc-runtime coupling this module removes). `fieldRustType` is the
1070
1133
  * ports-struct field type (from portFieldRustType); `resolveRef` resolves a ref head for the scalar path.
1071
1134
  */
1072
- function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan) {
1135
+ function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan, typedNodes) {
1073
1136
  const field = portFieldName(pn);
1074
1137
  // #110: a componentRef port bound to a runtime array input port (with elemType) → the native Vec<ElemT>,
1075
1138
  // cloned out of the concrete input struct (`in_.<field>.clone()`) — ZERO boxed Value on the read hot path.
@@ -1079,6 +1142,12 @@ function emitPortInit(pn, expr, fieldRustType, resolveRef, inputPorts, plan) {
1079
1142
  return `${field}: in_.${rustFieldName(e.path[0])}.clone()`;
1080
1143
  }
1081
1144
  }
1145
+ // #129: a leaf port that is a ref into a PRIOR NODE's typed arr outType (node→leaf array dataflow) →
1146
+ // the native Vec<ElemT> cloned off the prior node's typed cell (`t_<node>.borrow().field….clone()`) —
1147
+ // ZERO boxed Value on the read hot path (a fold/dedup leaf whose `items` = a prior node's array result).
1148
+ const priorArr = priorNodeArrElemInfo(expr, typedNodes, plan);
1149
+ if (priorArr !== null)
1150
+ return `${field}: ${priorArr.overExpr}`;
1082
1151
  // change #2: a static string array (projection) → a native `vec![...]` of &'static str.
1083
1152
  const strArr = staticStringArrElems(expr);
1084
1153
  if (strArr !== null) {
@@ -1165,7 +1234,8 @@ function emitNativeRawRunner(comp, plan, ap) {
1165
1234
  }
1166
1235
  const typedNodes = new Map();
1167
1236
  for (const n of comp.body)
1168
- typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
1237
+ if (!isControlGate(n))
1238
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan)); // #125: control-gate は typeless(typedNodes に載せない)
1169
1239
  const idToIndex = new Map();
1170
1240
  comp.body.forEach((n, i) => idToIndex.set(n.id, i));
1171
1241
  const opsLiteral = comp.body.map((n) => {
@@ -1258,8 +1328,13 @@ function emitNativeRawRunner(comp, plan, ap) {
1258
1328
  lines.push(` ${inParam}: ${inStructName(comp.name)},`);
1259
1329
  lines.push(`) -> Result<${outTy}, BehaviorError> {`);
1260
1330
  for (const k of order.keys()) {
1261
- const id = comp.body[order[k]].id;
1262
- lines.push(` let ${typedCell(id)}: RefCell<${renderTypeRef(typedNodes.get(id))}> = RefCell::new(Default::default());`);
1331
+ const nd = comp.body[order[k]];
1332
+ const id = nd.id;
1333
+ // #125: a control-gate cond has no typed data cell — only a produced_<gate> flag (its `if` result),
1334
+ // which its gated children read via the existing `if produced_<parent>.get()` skip-gate.
1335
+ if (!isControlGate(nd)) {
1336
+ lines.push(` let ${typedCell(id)}: RefCell<${renderTypeRef(typedNodes.get(id))}> = RefCell::new(Default::default());`);
1337
+ }
1263
1338
  lines.push(` let produced_${sanitize(id)} = std::cell::Cell::new(false);`);
1264
1339
  lines.push(` let _ = &produced_${sanitize(id)};`);
1265
1340
  }
@@ -1293,11 +1368,19 @@ function emitNativeRawRunner(comp, plan, ap) {
1293
1368
  lines.push(` // ── op '${node.id}' (${node.component}${node.relationKind ? ", relationKind:" + node.relationKind : ""}${node.parent ? ", parent:" + node.parent : ""}) ──`);
1294
1369
  let indent = " ";
1295
1370
  const closers = [];
1371
+ const parentIsGate = node.parent !== undefined && isControlGate(comp.body.find((b) => b.id === node.parent));
1296
1372
  if (node.parent !== undefined && priorNodeAt(node.parent, k)) {
1297
1373
  lines.push(`${indent}if produced_${sanitize(node.parent)}.get() {`);
1298
1374
  closers.unshift(`${indent}}`);
1299
1375
  indent += " ";
1300
- if (node.bindField !== undefined) {
1376
+ if (node.bindField !== undefined && parentIsGate) {
1377
+ // #125: a when(...) control-gate parent. gate の bindField:"ok" は run_behavior では成立時 `true`
1378
+ // / 不成立時 `null`(→ null-binding skip)。native では produced_<gate>(= gate の `if`)が既に
1379
+ // その skip を担うので、ここに来た時点で gate は成立。bound は skip の sentinel(データキーでは
1380
+ // ない — 子の ports は実データ親を参照する)として、非 None の `Option<String>` を渡す。
1381
+ lines.push(`${indent}let bound_${s}: ${boundRustType(node, typedNodes, plan)} = Some("ok".to_string());`);
1382
+ }
1383
+ else if (node.bindField !== undefined) {
1301
1384
  // bind (change #3 — typed produced-aware bound): read the parent struct field DIRECTLY (typed
1302
1385
  // access — no serialize). `bound_<s>` is a typed `Option<K>` — None => UNPRODUCED/skip, Some =>
1303
1386
  // produced (an empty string is a VALID produced value, `Some("")`). This is the EXACT `!= Null`
@@ -1323,8 +1406,8 @@ function emitNativeRawRunner(comp, plan, ap) {
1323
1406
  const inits = [];
1324
1407
  for (const pn of portNames) {
1325
1408
  const expr = node.ports[pn];
1326
- const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
1327
- inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan));
1409
+ const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1410
+ inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
1328
1411
  }
1329
1412
  const boundArg = node.bindField !== undefined ? `bound_${s}` : "None";
1330
1413
  // bc#97: derived per-node await — this point-read's future is awaited HERE (its result-consumption
@@ -1429,8 +1512,8 @@ function emitParallelStageArm(comp, stage, atPos, concurrency, metas, typedNodes
1429
1512
  const inits = [];
1430
1513
  for (const pn of Object.keys(node.ports)) {
1431
1514
  const expr = node.ports[pn];
1432
- const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan);
1433
- inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan));
1515
+ const ty = portFieldRustType(expr, comp.inputPorts, `port '${pn}' of node '${node.id}'`, undefined, plan, typedNodes);
1516
+ inits.push(emitPortInit(pn, expr, ty, resolveRef, comp.inputPorts, plan, typedNodes));
1434
1517
  }
1435
1518
  if (gated) {
1436
1519
  lines.push(`${ind}ports_${s} = Some(${structName} { ${inits.join(", ")} });`);
@@ -1862,7 +1945,6 @@ function rustInputHeadRef(head, comp, plan) {
1862
1945
  function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
1863
1946
  const c = node.cond;
1864
1947
  const s = sanitize(node.id);
1865
- const nodeRef = typedNodes.get(node.id);
1866
1948
  const priorHere = (head) => priorNodeAt(head, atPos);
1867
1949
  const resolveHead = (head) => {
1868
1950
  if (priorHere(head))
@@ -1870,8 +1952,29 @@ function emitCondArm(comp, node, atPos, typedNodes, plan, priorNodeAt) {
1870
1952
  return rustInputHeadRef(head, comp, plan);
1871
1953
  };
1872
1954
  const compiler = new NativeExprCompiler(makeRustExprBackend(resolveHead, plan));
1873
- const resolveDecl = (name) => findDecl(plan, name).fields;
1874
1955
  const cond = compiler.compileBool(c.if);
1956
+ // #125: a when(...) control-gate cond は出力データ型を持たない制御スキップマーカー。typed cell へ何も
1957
+ // materialize せず、`if` を native bool にして produced_<gate> に写すだけ。分岐内ノード
1958
+ // (`parent=gate, bindField:"ok"`)は既存の `if produced_<gate>.get()` skip-gate が実行選択する。
1959
+ if (isControlGate(node)) {
1960
+ const glines = [];
1961
+ glines.push(` // ── control-gate cond '${node.id}'${c.parent ? " (parent:" + c.parent + ")" : ""} — when(...) skip marker (no data type) ──`);
1962
+ let gindent = " ";
1963
+ const gclosers = [];
1964
+ if (c.parent !== undefined && priorNodeAt(c.parent, atPos)) {
1965
+ glines.push(`${gindent}if produced_${sanitize(c.parent)}.get() {`);
1966
+ gclosers.unshift(`${gindent}}`);
1967
+ gindent += " ";
1968
+ }
1969
+ for (const st of cond.stmts)
1970
+ glines.push(`${gindent}${st}`);
1971
+ glines.push(`${gindent}produced_${s}.set(${rustStripOuterParens(cond.expr)});`);
1972
+ for (const cl of gclosers)
1973
+ glines.push(cl);
1974
+ return glines.join("\n");
1975
+ }
1976
+ const nodeRef = typedNodes.get(node.id);
1977
+ const resolveDecl = (name) => findDecl(plan, name).fields;
1875
1978
  const thenC = compiler.compileTo(c.then, nodeRef, resolveDecl);
1876
1979
  const elseC = compiler.compileTo(c.else, nodeRef, resolveDecl);
1877
1980
  const lines = [];
@@ -2031,7 +2134,8 @@ ${emitStructSurface([])}
2031
2134
  const typedNodes = new Map();
2032
2135
  // build the full typed-node map FIRST so a map's over-element resolution can see every node.
2033
2136
  for (const n of c.body)
2034
- typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
2137
+ if (!isControlGate(n))
2138
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan)); // #125: control-gate は typeless(typedNodes に載せない)
2035
2139
  for (const n of c.body) {
2036
2140
  if ("fanout" in n)
2037
2141
  structs.push(emitFanoutPortsStructs(c, n, typedNodes, plan));
@@ -2041,7 +2145,7 @@ ${emitStructSurface([])}
2041
2145
  // #108: a cond node has NO ports struct (pure Expression — no handler dispatch).
2042
2146
  }
2043
2147
  else
2044
- structs.push(emitPortsStruct(c, n, undefined, plan));
2148
+ structs.push(emitPortsStruct(c, n, undefined, plan, typedNodes)); // #129: typedNodes for prior-node arr leaf ports.
2045
2149
  }
2046
2150
  rowStructs.push(emitRawRowStructs(c, typedNodes, plan));
2047
2151
  handlerTraits.push(emitHandlerTrait(c, typedNodes, plan, asyncPlans.get(c.name)));
@@ -2129,7 +2233,8 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2129
2233
  const fnKwFor = (nodeId) => (ap.nodeIsAsync(nodeId) ? "async fn" : "fn");
2130
2234
  const typedNodes = new Map();
2131
2235
  for (const n of c.body)
2132
- typedNodes.set(n.id, deriveTypeRef(n.outType, plan));
2236
+ if (!isControlGate(n))
2237
+ typedNodes.set(n.id, deriveTypeRef(n.outType, plan)); // #125: control-gate は typeless(typedNodes に載せない)
2133
2238
  const methods = [];
2134
2239
  for (const n of c.body) {
2135
2240
  if ("cond" in n)
@@ -2149,7 +2254,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2149
2254
  const elemDecode = emitDecodeRow(c.name, n.id, elemT, elemRowRef, plan, emittedDecode, decodeFns, "Elem");
2150
2255
  const portsT = `${portsStructName(c.name, n.id)}Batch`;
2151
2256
  methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
2152
- let (is_err, err, ok) = self.next(${rustStrLit(f.component)})?;
2257
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(f.component)})?;
2153
2258
  if is_err {
2154
2259
  return Some(${batchT} { is_error: true, err, ..Default::default() });
2155
2260
  }
@@ -2179,7 +2284,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2179
2284
  const retT = batched ? batchT : elemT;
2180
2285
  if (batched) {
2181
2286
  methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${batchT}> {
2182
- let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
2287
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
2183
2288
  if is_err {
2184
2289
  return Some(${batchT} { is_error: true, err, ..Default::default() });
2185
2290
  }
@@ -2190,7 +2295,7 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2190
2295
  }
2191
2296
  else {
2192
2297
  methods.push(` ${fnKw} ${nodeMethodName(n.id)}(&self, _ports: &${portsT}, _bound: ${bt}) -> Option<${elemT}> {
2193
- let (is_err, err, ok) = self.next(${rustStrLit(m.component)})?;
2298
+ let (is_err, err, ok, _echo) = self.next(${rustStrLit(m.component)})?;
2194
2299
  if is_err {
2195
2300
  return Some(${elemT} { is_error: true, err, ..Default::default() });
2196
2301
  }
@@ -2204,11 +2309,22 @@ export function rustTypedNativeObserve(ir, ioModel = "sync") {
2204
2309
  const node = n;
2205
2310
  const rowT = rawRowStructName(c.name, node.id);
2206
2311
  const decode = emitDecodeRow(c.name, node.id, rowT, ref, plan, emittedDecode, decodeFns, "");
2207
- methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, _ports: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
2208
- let (is_err, err, ok) = self.next(${rustStrLit(node.component)})?;
2312
+ // #129 (TEST glue): if this node has an `items` port and its outType is a bare arr, support the
2313
+ // reference scriptedHandlers' `echo === "items"` — the scripted handler ECHOES the NATIVE `f_items`
2314
+ // port back as its result. This makes the node→leaf array dataflow test non-vacuous: the leaf's
2315
+ // observed result is exactly the native array that flowed into its ports struct, so a dropped or
2316
+ // boxed native array would diverge from run_behavior. `ports` is bound only on the echo path.
2317
+ const canEcho = ref.kind === "arr" && Object.prototype.hasOwnProperty.call(node.ports, "items");
2318
+ const portsParam = canEcho ? "ports" : "_ports";
2319
+ const echoBranch = canEcho
2320
+ ? `\n if echo == "items" {\n return Some(${rowT} { val: ${portsParam}.${portFieldName("items")}.clone(), ..Default::default() });\n }`
2321
+ : "";
2322
+ const okBind = canEcho ? "(is_err, err, ok, echo)" : "(is_err, err, ok, _echo)";
2323
+ methods.push(` ${fnKw} ${nodeMethodName(node.id)}(&self, ${portsParam}: &${portsStructName(c.name, node.id)}, _bound: ${bt}) -> Option<${rowT}> {
2324
+ let ${okBind} = self.next(${rustStrLit(node.component)})?;
2209
2325
  if is_err {
2210
2326
  return Some(${rowT} { is_error: true, err, ..Default::default() });
2211
- }
2327
+ }${echoBranch}
2212
2328
  let v = ok.unwrap_or(Value::Null);
2213
2329
  Some(${decode}(&v))
2214
2330
  }`);
@@ -2263,19 +2379,22 @@ impl ScriptedNativeRaw {
2263
2379
 
2264
2380
  // drain one scripted outcome for a component (last entry repeats — mirrors the reference scripted
2265
2381
  // handlers); None when the component has no scripted queue (fail-closed). Returns (is_error, err,
2266
- // ok_value). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain is
2267
- // Mutex-guarded.
2268
- fn next(&self, component: &str) -> Option<(bool, String, Option<Value>)> {
2382
+ // ok_value, echo). The bc#87 parallel-stage runner calls node_* from multiple threads, so the drain
2383
+ // is Mutex-guarded. #129: \`echo\` carries the reference scriptedHandlers' \`echo\` directive (e.g.
2384
+ // "items") so a scripted node can ECHO a named input PORT back — used to make the node→leaf array
2385
+ // dataflow test non-vacuous (the leaf returns its \`items\` port; a dropped/boxed array diverges).
2386
+ fn next(&self, component: &str) -> Option<(bool, String, Option<Value>, String)> {
2269
2387
  self.seen.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2270
2388
  let mut queues = self.queues.lock().unwrap();
2271
2389
  let (queue, single) = queues.get_mut(component)?;
2272
2390
  let raw = if *single || queue.len() <= 1 { queue[0].clone() } else { queue.remove(0) };
2391
+ let echo = raw.get("echo").and_then(|e| e.as_str()).unwrap_or("").to_string();
2273
2392
  if let Some(okv) = raw.get("ok") {
2274
2393
  let v = behavior_contracts::decode_value(okv).expect("decode ok");
2275
- Some((false, String::new(), Some(v)))
2394
+ Some((false, String::new(), Some(v), echo))
2276
2395
  } else {
2277
2396
  let e = raw.get("error").and_then(|e| e.as_str()).unwrap_or("").to_string();
2278
- Some((true, e, None))
2397
+ Some((true, e, None, echo))
2279
2398
  }
2280
2399
  }
2281
2400
  }`;
@@ -2442,6 +2561,7 @@ function emitDecodeRow(compName, nodeId, rowT, ref, plan, emitted, out, suffix)
2442
2561
  }
2443
2562
  export const rustTypedNativeEmitter = {
2444
2563
  language: "rust-typed-native",
2564
+ classification: "codegen", // native: 型付きフラットコード生成・IR/dict 非在(#128/A6)
2445
2565
  fileExtension: ".rs",
2446
2566
  defaultRuntimeImport: "behavior_contracts",
2447
2567
  filenameHint: "behaviors_typed_native_generated.rs",