behavior-contracts 0.7.2__tar.gz → 0.7.3__tar.gz

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 (27) hide show
  1. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/PKG-INFO +3 -3
  2. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/README.md +2 -2
  3. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/pyproject.toml +1 -1
  4. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/__init__.py +3 -1
  5. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/behavior.py +70 -2
  6. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts.egg-info/PKG-INFO +3 -3
  7. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/tests/test_codegen_generated.py +1 -1
  8. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/tests/test_conformance.py +1 -1
  9. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/tests/test_public_api.py +1 -1
  10. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/setup.cfg +0 -0
  11. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/canonical.py +0 -0
  12. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/codec.py +0 -0
  13. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/conformance.py +0 -0
  14. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/envelope.py +0 -0
  15. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/expr_eval.py +0 -0
  16. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/fingerprint.py +0 -0
  17. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/guard.py +0 -0
  18. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/plan.py +0 -0
  19. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/primitives.py +0 -0
  20. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts/template.py +0 -0
  21. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts.egg-info/SOURCES.txt +0 -0
  22. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts.egg-info/dependency_links.txt +0 -0
  23. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts.egg-info/entry_points.txt +0 -0
  24. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts.egg-info/requires.txt +0 -0
  25. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/src/behavior_contracts.egg-info/top_level.txt +0 -0
  26. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/tests/test_plan_parallel.py +0 -0
  27. {behavior_contracts-0.7.2 → behavior_contracts-0.7.3}/tests/test_primitives.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: behavior-contracts
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Multi-language IR-Runtime common core (Python port). The COMMON 'thin core' primitives from dsl-contracts runtime-boundary.md — validate_envelope / evaluate_expression / render_template / run_plan / canonical_value / canonical_json / py_float_repr / assert_portable — with zero backend (DynamoDB/graphddb) dependencies.
5
5
  Author: foo-log
6
6
  License: MIT
@@ -60,12 +60,12 @@ string matching the conformance protocol's failure-code set.
60
60
 
61
61
  ## Conformance
62
62
 
63
- Runs the 7 shared suites (164 vectors) from `../conformance/vectors/*.json`
63
+ Runs the 7 shared suites (165 vectors) from `../conformance/vectors/*.json`
64
64
  through this package per [`../conformance/PROTOCOL.md`](../conformance/PROTOCOL.md),
65
65
  including the pre-flight version fail-closed sweep.
66
66
 
67
67
  ```bash
68
- python -m behavior_contracts.conformance # → "164 passed, 0 failed / 164 vectors across 7 suites"
68
+ python -m behavior_contracts.conformance # → "164 passed, 0 failed / 165 vectors across 7 suites"
69
69
  # or the console script:
70
70
  behavior-contracts-conformance
71
71
  ```
@@ -48,12 +48,12 @@ string matching the conformance protocol's failure-code set.
48
48
 
49
49
  ## Conformance
50
50
 
51
- Runs the 7 shared suites (164 vectors) from `../conformance/vectors/*.json`
51
+ Runs the 7 shared suites (165 vectors) from `../conformance/vectors/*.json`
52
52
  through this package per [`../conformance/PROTOCOL.md`](../conformance/PROTOCOL.md),
53
53
  including the pre-flight version fail-closed sweep.
54
54
 
55
55
  ```bash
56
- python -m behavior_contracts.conformance # → "164 passed, 0 failed / 164 vectors across 7 suites"
56
+ python -m behavior_contracts.conformance # → "164 passed, 0 failed / 165 vectors across 7 suites"
57
57
  # or the console script:
58
58
  behavior-contracts-conformance
59
59
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "behavior-contracts"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "Multi-language IR-Runtime common core (Python port). The COMMON 'thin core' primitives from dsl-contracts runtime-boundary.md — validate_envelope / evaluate_expression / render_template / run_plan / canonical_value / canonical_json / py_float_repr / assert_portable — with zero backend (DynamoDB/graphddb) dependencies."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -22,7 +22,7 @@ SPEC_VERSIONS = {
22
22
  "template": 1,
23
23
  "plan": 1,
24
24
  "canonical": 2, # v2: obj 値の __proto__ own key を FORBIDDEN_KEY に(fail-closed)
25
- "behavior": 2, # v2: map.when / map.into / map.batched / handler ctx(bc#22。v1 IR の意味論は不変)
25
+ "behavior": 3, # v3: first-class fanout node kind(deduped connection list-in fan-out)。v1/v2 意味論は不変(additive)
26
26
  "guard": 1, # 初版(bc#25: assert_portable_component_graph の accept/reject を vector-pin)
27
27
  "c2": 1, # 初版(bc#28: c2-catalog-swap — catalog-swap 実行 + IR 構造一致の 5 言語 pin)
28
28
  }
@@ -80,6 +80,7 @@ from .guard import ( # noqa: E402
80
80
  # ── behavior(component-graph IR + run_behavior) ──────────────────────────────
81
81
  from .behavior import ( # noqa: E402
82
82
  BehaviorFailure,
83
+ fanout_dedup_drop,
83
84
  run_behavior,
84
85
  )
85
86
 
@@ -143,6 +144,7 @@ __all__ = [
143
144
  "PortabilityError",
144
145
  # behavior
145
146
  "run_behavior",
147
+ "fanout_dedup_drop",
146
148
  "BehaviorFailure",
147
149
  # codec
148
150
  "decode_value",
@@ -52,16 +52,20 @@ def _bfail(code: str, message: str) -> "None":
52
52
 
53
53
 
54
54
  def _node_kind(n: Mapping[str, Any]) -> str:
55
+ if "fanout" in n:
56
+ return "fanout"
55
57
  if "map" in n:
56
58
  return "map"
57
59
  if "cond" in n:
58
60
  return "cond"
59
61
  if "component" in n:
60
62
  return "componentRef"
61
- _bfail("UNKNOWN_NODE_KIND", f"body node '{n.get('id', '?')}' is not componentRef/map/cond")
63
+ _bfail("UNKNOWN_NODE_KIND", f"body node '{n.get('id', '?')}' is not componentRef/map/cond/fanout")
62
64
 
63
65
 
64
66
  def _node_parent(n: Mapping[str, Any]) -> Optional[str]:
67
+ if "fanout" in n:
68
+ return n["fanout"].get("parent")
65
69
  if "map" in n:
66
70
  return n["map"].get("parent")
67
71
  if "cond" in n:
@@ -70,12 +74,14 @@ def _node_parent(n: Mapping[str, Any]) -> Optional[str]:
70
74
 
71
75
 
72
76
  def _node_bind_field(n: Mapping[str, Any]) -> Optional[str]:
73
- if "map" in n or "cond" in n:
77
+ if "map" in n or "cond" in n or "fanout" in n:
74
78
  return None
75
79
  return n.get("bindField")
76
80
 
77
81
 
78
82
  def _node_policy(n: Mapping[str, Any]) -> Optional[str]:
83
+ if "fanout" in n:
84
+ return n["fanout"].get("policy")
79
85
  if "map" in n:
80
86
  return n["map"].get("policy")
81
87
  if "cond" in n:
@@ -84,6 +90,8 @@ def _node_policy(n: Mapping[str, Any]) -> Optional[str]:
84
90
 
85
91
 
86
92
  def _node_relation_kind(n: Mapping[str, Any]) -> Optional[str]:
93
+ if "fanout" in n:
94
+ return n["fanout"].get("relationKind")
87
95
  if "map" in n:
88
96
  return n["map"].get("relationKind")
89
97
  if "cond" in n:
@@ -91,6 +99,37 @@ def _node_relation_kind(n: Mapping[str, Any]) -> Optional[str]:
91
99
  return n.get("relationKind")
92
100
 
93
101
 
102
+ def fanout_dedup_drop(aligned_bodies: List[Value], dedupe_key: str, drop: str,
103
+ implicit_source: Optional[str]) -> List[Value]:
104
+ """THE ONE dedup/drop definition (behaviorVersion 3) — Python twin of behavior.ts fanoutDedupDrop.
105
+
106
+ Applies first-seen dedupe (by the body's ``dedupe_key`` field) + dangling drop (null/non-object/
107
+ absent-key body when ``drop=='dangling'``) + implicitSource strip to the aligned raw list; returns
108
+ the connection ``items`` (the wrap is done by the caller). MUST stay byte-equal to the TS/native defs.
109
+ """
110
+ items: List[Value] = []
111
+ seen: set = set()
112
+ for body in aligned_bodies:
113
+ rec = body if isinstance(body, dict) else None
114
+ key_val = rec.get(dedupe_key) if rec is not None else None
115
+ has_key = key_val is not None
116
+ if not has_key:
117
+ if drop == "dangling":
118
+ continue
119
+ items.append(body)
120
+ continue
121
+ seen_key = ("s:" + key_val) if isinstance(key_val, str) else ("j:" + repr(key_val))
122
+ if seen_key in seen:
123
+ continue
124
+ seen.add(seen_key)
125
+ if implicit_source is not None and rec is not None and implicit_source in rec:
126
+ copy = {k: v for k, v in rec.items() if k != implicit_source}
127
+ items.append(copy)
128
+ else:
129
+ items.append(body)
130
+ return items
131
+
132
+
94
133
  def _eval_ports(ports: Mapping[str, Any], scope: Dict[str, Value]) -> Dict[str, Value]:
95
134
  # key はソートせず宣言順のまま。FORBIDDEN_KEY 等は evaluate が投げる(fail-closed)。
96
135
  return {k: evaluate_expression(v, scope) for k, v in ports.items()}
@@ -235,6 +274,35 @@ def run_behavior(
235
274
  augmented.append(el)
236
275
  return {"ok": augmented}
237
276
 
277
+ if kind == "fanout":
278
+ # fanout(v3): over(id-list)→ dedup 済み 1 回の batched handler → dedupe/drop/strip →
279
+ # connection {items, cursor:None}。整列制約(MAP_BATCH_RESULT_MISMATCH)は適用されない。
280
+ f = node["fanout"]
281
+ over = evaluate_expression(f["over"], base_scope())
282
+ if not isinstance(over, list):
283
+ _bfail("FANOUT_OVER_NOT_ARRAY", f"fanout '{op['id']}': 'over' did not evaluate to an array")
284
+ handler = handlers.get(f["component"])
285
+ if handler is None:
286
+ _bfail("UNKNOWN_COMPONENT", f"component '{f['component']}' has no handler (fail-closed)")
287
+ items_ports: List[Value] = []
288
+ for el in over:
289
+ scope = {**base_scope(), f["as"]: el}
290
+ items_ports.append(_eval_ports(f["ports"], scope))
291
+ if not items_ports:
292
+ return {"ok": {"items": [], "cursor": None}}
293
+ outcome = handler({"items": items_ports}, {"nodeId": op["id"], "component": f["component"]})
294
+ if "error" in outcome:
295
+ return outcome
296
+ r = outcome["ok"]
297
+ if not isinstance(r, list) or len(r) != len(items_ports):
298
+ _bfail(
299
+ "FANOUT_BATCH_RESULT_MISMATCH",
300
+ f"fanout '{op['id']}': batched handler must return a list aligned to the deduped id list "
301
+ f"(want {len(items_ports)}, got {len(r) if isinstance(r, list) else type(r).__name__})",
302
+ )
303
+ items_out = fanout_dedup_drop(r, f["dedupeKey"], f["drop"], f.get("implicitSource"))
304
+ return {"ok": {"items": items_out, "cursor": None}}
305
+
238
306
  # componentRef
239
307
  handler = handlers.get(node["component"])
240
308
  if handler is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: behavior-contracts
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Multi-language IR-Runtime common core (Python port). The COMMON 'thin core' primitives from dsl-contracts runtime-boundary.md — validate_envelope / evaluate_expression / render_template / run_plan / canonical_value / canonical_json / py_float_repr / assert_portable — with zero backend (DynamoDB/graphddb) dependencies.
5
5
  Author: foo-log
6
6
  License: MIT
@@ -60,12 +60,12 @@ string matching the conformance protocol's failure-code set.
60
60
 
61
61
  ## Conformance
62
62
 
63
- Runs the 7 shared suites (164 vectors) from `../conformance/vectors/*.json`
63
+ Runs the 7 shared suites (165 vectors) from `../conformance/vectors/*.json`
64
64
  through this package per [`../conformance/PROTOCOL.md`](../conformance/PROTOCOL.md),
65
65
  including the pre-flight version fail-closed sweep.
66
66
 
67
67
  ```bash
68
- python -m behavior_contracts.conformance # → "164 passed, 0 failed / 164 vectors across 7 suites"
68
+ python -m behavior_contracts.conformance # → "164 passed, 0 failed / 165 vectors across 7 suites"
69
69
  # or the console script:
70
70
  behavior-contracts-conformance
71
71
  ```
@@ -110,7 +110,7 @@ def test_tampered_generated_module_is_loudly_rejected(tmp_path):
110
110
  code = f.read()
111
111
 
112
112
  # spec-version skew(焼き込み定数の改変)→ ロード時 RuntimeError。
113
- skew = code.replace('"behavior": 2', '"behavior": 99')
113
+ skew = code.replace('"behavior": 3', '"behavior": 99')
114
114
  assert skew != code
115
115
  p = tmp_path / "skew.py"
116
116
  p.write_text(skew, encoding="utf-8")
@@ -1,4 +1,4 @@
1
- """conformance runner を pytest から実行する(164 vectors 全 PASS を要求)。"""
1
+ """conformance runner を pytest から実行する(165 vectors 全 PASS を要求)。"""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -469,7 +469,7 @@ class TestValidateEnvelope:
469
469
  "template": 1,
470
470
  "plan": 1,
471
471
  "canonical": 2,
472
- "behavior": 2,
472
+ "behavior": 3,
473
473
  "guard": 1,
474
474
  "c2": 1,
475
475
  }