behavior-contracts 0.7.2__tar.gz → 0.8.0__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.8.0}/PKG-INFO +3 -3
  2. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/README.md +2 -2
  3. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/pyproject.toml +1 -1
  4. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/__init__.py +5 -2
  5. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/behavior.py +70 -2
  6. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/conformance.py +41 -1
  7. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts.egg-info/PKG-INFO +3 -3
  8. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/tests/test_codegen_generated.py +1 -1
  9. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/tests/test_conformance.py +1 -1
  10. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/tests/test_public_api.py +3 -2
  11. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/setup.cfg +0 -0
  12. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/canonical.py +0 -0
  13. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/codec.py +0 -0
  14. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/envelope.py +0 -0
  15. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/expr_eval.py +0 -0
  16. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/fingerprint.py +0 -0
  17. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/guard.py +0 -0
  18. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/plan.py +0 -0
  19. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/primitives.py +0 -0
  20. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts/template.py +0 -0
  21. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts.egg-info/SOURCES.txt +0 -0
  22. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts.egg-info/dependency_links.txt +0 -0
  23. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts.egg-info/entry_points.txt +0 -0
  24. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts.egg-info/requires.txt +0 -0
  25. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/src/behavior_contracts.egg-info/top_level.txt +0 -0
  26. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/tests/test_plan_parallel.py +0 -0
  27. {behavior_contracts-0.7.2 → behavior_contracts-0.8.0}/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.8.0
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 8 shared suites (173 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 # → "173 passed, 0 failed / 173 vectors across 8 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 8 shared suites (173 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 # → "173 passed, 0 failed / 173 vectors across 8 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.8.0"
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,9 +22,10 @@ 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 の意味論は不変)
26
- "guard": 1, # 初版(bc#25: assert_portable_component_graph の accept/reject を vector-pin
25
+ "behavior": 4, # v4(#128/A6・SCP-only): provenance token + 全ノード型確定を serialized-IR 不変条件へ格上げ(released 3=fanout を retire)。v1–v3 意味論は不変だが旧 runtime(behavior≤3)は新 IR を loud reject する(機構 = 生成モジュールの baked-spec-skew ゲート[EXPECTED_SPEC_VERSIONS.behavior vs runtime] + conformance pre-flight version fail-closed。validate_envelope は envelope の specVersion「major.minor」文字列を見るのであって behaviorVersion 整数ではない)
26
+ "guard": 2, # v2(#128/A6): assert_compiled + UNTYPED_NODE + operator 型シグネチャ SSoT の accept/reject を vector-pin(v1 は bc#25 の可搬性 pin のみ)
27
27
  "c2": 1, # 初版(bc#28: c2-catalog-swap — catalog-swap 実行 + IR 構造一致の 5 言語 pin)
28
+ "provenance": 1, # 初版(#128/A6): シリアライズ境界の canonical fingerprint 照合(改竄/stale の 5 言語 loud reject)+ 非空虚性(CONFORMANCE_MUTATE)
28
29
  }
29
30
 
30
31
  # validate_envelope に渡す graphddb 形式の既定 supported 版(``"<major>.<minor>"``)。
@@ -80,6 +81,7 @@ from .guard import ( # noqa: E402
80
81
  # ── behavior(component-graph IR + run_behavior) ──────────────────────────────
81
82
  from .behavior import ( # noqa: E402
82
83
  BehaviorFailure,
84
+ fanout_dedup_drop,
83
85
  run_behavior,
84
86
  )
85
87
 
@@ -143,6 +145,7 @@ __all__ = [
143
145
  "PortabilityError",
144
146
  # behavior
145
147
  "run_behavior",
148
+ "fanout_dedup_drop",
146
149
  "BehaviorFailure",
147
150
  # codec
148
151
  "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:
@@ -39,6 +39,7 @@ from . import (
39
39
  encode_value,
40
40
  evaluate_expression,
41
41
  final_tree,
42
+ fingerprint_component_graph,
42
43
  py_float_repr,
43
44
  render_template,
44
45
  run_behavior,
@@ -93,6 +94,7 @@ def _preflight(vectors_dir: Path) -> Dict[str, Any]:
93
94
  ("behavior.json", "behavior", "behaviorVersion", SPEC_VERSIONS["behavior"]),
94
95
  ("guard.json", "guard", "guardVersion", SPEC_VERSIONS["guard"]),
95
96
  ("c2-catalog-swap.json", "c2", "c2Version", SPEC_VERSIONS["c2"]),
97
+ ("provenance.json", "provenance", "provenanceVersion", SPEC_VERSIONS["provenance"]),
96
98
  ]
97
99
  loaded = [(f, suite, vk, want, _load_json(vectors_dir, f)) for (f, suite, vk, want) in specs]
98
100
  mismatches = [(suite, doc.get(vk), want) for (_, suite, vk, want, doc) in loaded if doc.get(vk) != want]
@@ -356,6 +358,43 @@ def _run_guard(t: _Tally, doc: Any) -> None:
356
358
  _bump(t, ok)
357
359
 
358
360
 
361
+ # ── provenance suite(シリアライズ境界の canonical fingerprint 照合, #128/A6)──────
362
+ def _run_provenance(t: _Tally, doc: Any) -> None:
363
+ """シリアライズ境界の provenance ゲート(PROTOCOL.md §3.8 / scp-only-authoring-contract §3 SA3・§5)。
364
+
365
+ token はプロセス内 capability ゆえ JSON 境界を越えない。境界のゲートは canonical fingerprint:
366
+ runner は `ir`(RAW JSON)から `fingerprint_component_graph` を **再計算** し、宣言 `fingerprint` と
367
+ 照合する。一致 = accept、不一致(改竄/stale)= 固定コード ``NON_COMPILED_IR`` で reject。
368
+ key 順・int/整数 float の綴りは fingerprint を変えない(canonical 不変性)。非空虚性
369
+ (CONFORMANCE_MUTATE): 1 nibble 崩した fingerprint の vector が必ず reject になることで、
370
+ runner が実際に再計算+照合していることを証明する。
371
+ """
372
+ print(f"\nprovenance.json (v{doc['provenanceVersion']}) — {len(doc['vectors'])} vectors")
373
+ for v in doc["vectors"]:
374
+ ok = False
375
+ detail = ""
376
+ recomputed = fingerprint_component_graph(v["ir"])
377
+ matched = recomputed == v["fingerprint"]
378
+ if matched:
379
+ if "ok" in v["expect"]:
380
+ ok = True
381
+ else:
382
+ detail = f"expected Failure({v['expect']['failure']}), fingerprint matched"
383
+ else:
384
+ # 不一致 = 境界 loader の NON_COMPILED_IR reject に相当。
385
+ if "failure" in v["expect"]:
386
+ ok = v["expect"]["failure"] == "NON_COMPILED_IR"
387
+ if not ok:
388
+ detail = f"expected Failure({v['expect']['failure']}), got NON_COMPILED_IR"
389
+ else:
390
+ detail = (
391
+ f"expected accept, got NON_COMPILED_IR "
392
+ f"(declared {v['fingerprint']}, recomputed {recomputed})"
393
+ )
394
+ _line(ok, v["name"], detail)
395
+ _bump(t, ok)
396
+
397
+
359
398
  # ── c2 suite(c2-catalog-swap: catalog-swap 実行 + IR 構造一致, bc#28)──────────
360
399
  def _c2_normalize(decoded_ir: Any, role_by_name: Dict[str, str]) -> Any:
361
400
  """component leaf 名を role に置換(構造は不変)。unmapped は ValueError。"""
@@ -453,9 +492,10 @@ def run(vectors_dir: Optional[Path] = None) -> int:
453
492
  _run_behavior(t, docs["behavior"])
454
493
  _run_guard(t, docs["guard"])
455
494
  _run_c2(t, docs["c2"])
495
+ _run_provenance(t, docs["provenance"])
456
496
 
457
497
  total = t.passed + t.failed
458
- print(f"\n{t.passed} passed, {t.failed} failed / {total} vectors across 7 suites")
498
+ print(f"\n{t.passed} passed, {t.failed} failed / {total} vectors across 8 suites")
459
499
  return t.failed
460
500
 
461
501
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: behavior-contracts
3
- Version: 0.7.2
3
+ Version: 0.8.0
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 8 shared suites (173 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 # → "173 passed, 0 failed / 173 vectors across 8 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": 4', '"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,9 +469,10 @@ class TestValidateEnvelope:
469
469
  "template": 1,
470
470
  "plan": 1,
471
471
  "canonical": 2,
472
- "behavior": 2,
473
- "guard": 1,
472
+ "behavior": 4,
473
+ "guard": 2,
474
474
  "c2": 1,
475
+ "provenance": 1,
475
476
  }
476
477
 
477
478