behavior-contracts 0.2.0__tar.gz → 0.2.2__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.
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/PKG-INFO +3 -3
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/README.md +2 -2
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/pyproject.toml +1 -1
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/__init__.py +25 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/guard.py +62 -0
- behavior_contracts-0.2.2/src/behavior_contracts/primitives.py +285 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/PKG-INFO +3 -3
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/SOURCES.txt +2 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/tests/test_conformance.py +1 -1
- behavior_contracts-0.2.2/tests/test_primitives.py +142 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/tests/test_public_api.py +102 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/setup.cfg +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/behavior.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/canonical.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/codec.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/conformance.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/envelope.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/expr_eval.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/fingerprint.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/plan.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts/template.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/dependency_links.txt +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/entry_points.txt +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/requires.txt +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/top_level.txt +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/tests/test_codegen_generated.py +0 -0
- {behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/tests/test_plan_parallel.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: behavior-contracts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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 (
|
|
63
|
+
Runs the 7 shared suites (164 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 # → "
|
|
68
|
+
python -m behavior_contracts.conformance # → "164 passed, 0 failed / 164 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 (
|
|
51
|
+
Runs the 7 shared suites (164 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 # → "
|
|
56
|
+
python -m behavior_contracts.conformance # → "164 passed, 0 failed / 164 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.2.
|
|
7
|
+
version = "0.2.2"
|
|
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"
|
|
@@ -70,9 +70,11 @@ from .envelope import ( # noqa: E402
|
|
|
70
70
|
# ── guard(Portability Guard) ────────────────────────────────────────────────
|
|
71
71
|
from .guard import ( # noqa: E402
|
|
72
72
|
PORTABLE_EXPR_OPERATORS,
|
|
73
|
+
PORTABLE_SCALAR_TYPES,
|
|
73
74
|
PortabilityError,
|
|
74
75
|
assert_portable,
|
|
75
76
|
assert_portable_component_graph,
|
|
77
|
+
assert_portable_type_notation,
|
|
76
78
|
)
|
|
77
79
|
|
|
78
80
|
# ── behavior(component-graph IR + run_behavior) ──────────────────────────────
|
|
@@ -93,6 +95,20 @@ from .fingerprint import ( # noqa: E402
|
|
|
93
95
|
fingerprint_component_graph,
|
|
94
96
|
)
|
|
95
97
|
|
|
98
|
+
# ── codegen primitive surface(bc#36 / typed-codegen.md §3.4)──────────────────
|
|
99
|
+
# 生成コードが直呼びできる安定 primitive 公開面。式演算子は個別関数として ``primitives``
|
|
100
|
+
# サブモジュール(``behavior_contracts.primitives``)で公開する。意味論は SSoT
|
|
101
|
+
# ``evaluate_expression`` へ委譲する薄いラッパ(再実装しない)。bounded 並行 /
|
|
102
|
+
# skip・unproduced / hydration helper も top-level に公開する。
|
|
103
|
+
from . import primitives # noqa: E402
|
|
104
|
+
from .primitives import ( # noqa: E402
|
|
105
|
+
hydrate_into,
|
|
106
|
+
map_with_concurrency,
|
|
107
|
+
skip_connection,
|
|
108
|
+
skip_single,
|
|
109
|
+
unproduced_value,
|
|
110
|
+
)
|
|
111
|
+
|
|
96
112
|
__all__ = [
|
|
97
113
|
"SPEC_VERSIONS",
|
|
98
114
|
"ENVELOPE_SPEC_VERSION",
|
|
@@ -121,7 +137,9 @@ __all__ = [
|
|
|
121
137
|
# guard
|
|
122
138
|
"assert_portable",
|
|
123
139
|
"assert_portable_component_graph",
|
|
140
|
+
"assert_portable_type_notation",
|
|
124
141
|
"PORTABLE_EXPR_OPERATORS",
|
|
142
|
+
"PORTABLE_SCALAR_TYPES",
|
|
125
143
|
"PortabilityError",
|
|
126
144
|
# behavior
|
|
127
145
|
"run_behavior",
|
|
@@ -132,4 +150,11 @@ __all__ = [
|
|
|
132
150
|
# generator 支援(fingerprint)
|
|
133
151
|
"fingerprint_component_graph",
|
|
134
152
|
"FingerprintFailure",
|
|
153
|
+
# codegen primitive surface (bc#36)
|
|
154
|
+
"primitives",
|
|
155
|
+
"map_with_concurrency",
|
|
156
|
+
"unproduced_value",
|
|
157
|
+
"skip_single",
|
|
158
|
+
"skip_connection",
|
|
159
|
+
"hydrate_into",
|
|
135
160
|
]
|
|
@@ -66,6 +66,62 @@ PORTABLE_EXPR_OPERATORS = frozenset(
|
|
|
66
66
|
}
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
+
# 可搬型記法(Portable Type Notation, B0 / bc#44 — scp-ir-architecture.md §5.2)の
|
|
70
|
+
# スカラ閉集合。複合型は `{opt|arr|obj: …}` の単一キー object のみ。
|
|
71
|
+
PORTABLE_SCALAR_TYPES = frozenset({"string", "int", "float", "bool", "null"})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def assert_portable_type_notation(node: Any, path: str) -> None:
|
|
75
|
+
"""可搬型記法(§5.2)を Expression IR と同じ強度で検証する(fail-closed)。
|
|
76
|
+
|
|
77
|
+
閉集合:
|
|
78
|
+
- スカラ: PORTABLE_SCALAR_TYPES(文字列のみ・``"any"`` エスケープ無し)。
|
|
79
|
+
- ``{opt: T}`` / ``{arr: T}``: 単一キー object・値は型 T(再帰)。
|
|
80
|
+
- ``{obj: {field: T, …}}``: 単一キー object・値は field→型の object(``__proto__`` キー禁止)。
|
|
81
|
+
|
|
82
|
+
:raises PortabilityError: 閉集合外の型記法を検出したとき。
|
|
83
|
+
"""
|
|
84
|
+
if isinstance(node, str):
|
|
85
|
+
if node not in PORTABLE_SCALAR_TYPES:
|
|
86
|
+
raise PortabilityError(
|
|
87
|
+
path, f"Portability Guard: unknown scalar type '{node}' (fail-closed) at {path}"
|
|
88
|
+
)
|
|
89
|
+
return
|
|
90
|
+
if not isinstance(node, dict):
|
|
91
|
+
raise PortabilityError(
|
|
92
|
+
path,
|
|
93
|
+
f"Portability Guard: type notation must be a scalar-type string or a single-key {{opt|arr|obj}} object (fail-closed) at {path}",
|
|
94
|
+
)
|
|
95
|
+
keys = list(node.keys())
|
|
96
|
+
if len(keys) != 1:
|
|
97
|
+
raise PortabilityError(
|
|
98
|
+
path,
|
|
99
|
+
f"Portability Guard: compound type must be a single-key object (opt|arr|obj), got {len(keys)} keys (fail-closed) at {path}",
|
|
100
|
+
)
|
|
101
|
+
kind = keys[0]
|
|
102
|
+
arg = node[kind]
|
|
103
|
+
if kind in ("opt", "arr"):
|
|
104
|
+
assert_portable_type_notation(arg, f"{path}.{kind}")
|
|
105
|
+
return
|
|
106
|
+
if kind == "obj":
|
|
107
|
+
if not isinstance(arg, dict):
|
|
108
|
+
raise PortabilityError(
|
|
109
|
+
f"{path}.obj",
|
|
110
|
+
f"Portability Guard: {{obj: ...}} type expects a field->type object (fail-closed) at {path}.obj",
|
|
111
|
+
)
|
|
112
|
+
for k, v in arg.items():
|
|
113
|
+
if k == FORBIDDEN_OBJECT_KEY:
|
|
114
|
+
raise PortabilityError(
|
|
115
|
+
f"{path}.obj",
|
|
116
|
+
f'Portability Guard: object type key "{FORBIDDEN_OBJECT_KEY}" is forbidden (fail-closed) at {path}.obj',
|
|
117
|
+
)
|
|
118
|
+
assert_portable_type_notation(v, f"{path}.obj.{k}")
|
|
119
|
+
return
|
|
120
|
+
raise PortabilityError(
|
|
121
|
+
path,
|
|
122
|
+
f"Portability Guard: unknown compound type kind '{kind}' (opt|arr|obj) (fail-closed) at {path}",
|
|
123
|
+
)
|
|
124
|
+
|
|
69
125
|
|
|
70
126
|
def assert_portable_component_graph(ir: Any, path: str = "$") -> None:
|
|
71
127
|
"""component-graph IR(scp-ir-architecture.md §5)の可搬性 Guard(P0-2)。
|
|
@@ -99,6 +155,9 @@ def _assert_portable_component(c: Any, path: str) -> None:
|
|
|
99
155
|
for ni, n in enumerate(body):
|
|
100
156
|
_assert_portable_body_node(n, f"{path}.body[{ni}]")
|
|
101
157
|
_assert_portable_expr(c.get("output"), f"{path}.output")
|
|
158
|
+
# 可搬型記法(B0 / bc#44・§5.2): outputType は additive・省略可。
|
|
159
|
+
if "outputType" in c:
|
|
160
|
+
assert_portable_type_notation(c.get("outputType"), f"{path}.outputType")
|
|
102
161
|
|
|
103
162
|
|
|
104
163
|
def _assert_portable_body_node(n: Any, path: str) -> None:
|
|
@@ -132,6 +191,9 @@ def _assert_portable_body_node(n: Any, path: str) -> None:
|
|
|
132
191
|
_assert_portable_ports(n.get("ports"), f"{path}.ports")
|
|
133
192
|
else:
|
|
134
193
|
raise PortabilityError(path, f"Portability Guard: unknown body node kind (not componentRef/map/cond) at {path}")
|
|
194
|
+
# 可搬型記法(B0 / bc#44・§5.2): node の outType は additive・省略可(全ノード種で共通)。
|
|
195
|
+
if "outType" in n:
|
|
196
|
+
assert_portable_type_notation(n.get("outType"), f"{path}.outType")
|
|
135
197
|
|
|
136
198
|
|
|
137
199
|
def _require_string_component(v: Any, path: str) -> None:
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""primitives.py — codegen が直呼びできる安定 primitive 公開面(bc#36 / typed-codegen.md §3.4)。
|
|
2
|
+
|
|
3
|
+
A0 = 公開面の確定(emitter 生成ロジックは A1/#37)。生成コードが「細粒度 primitive を
|
|
4
|
+
呼ぶ」ための callable surface を additive に切り出す。
|
|
5
|
+
|
|
6
|
+
最重要規律(AC 7): **演算子意味論は再実装しない。** 各 primitive は Expression IR の
|
|
7
|
+
単一キーノードを組んで既存 SSoT ``evaluate``(expr_eval.py)へ委譲するだけの薄いラッパ。
|
|
8
|
+
よって意味論(INT_OVERFLOW / PRECISION_LOSS / MOD_ZERO / NULL_REF / TYPE_MISMATCH /
|
|
9
|
+
短絡未評価 / code-point 順 / CPython float repr 等)は ``evaluate`` と**完全一致**する。
|
|
10
|
+
|
|
11
|
+
短絡の保存: ``and_`` / ``or_`` / ``cond`` / ``coalesce`` は **未評価の IR ノード**を引数に
|
|
12
|
+
取り、内部で ``evaluate`` が短絡する(値を先に渡す API は採らない)。
|
|
13
|
+
|
|
14
|
+
加えて run_plan / bounded 並行 / skip・unproduced / hydration helper を再エクスポートまたは
|
|
15
|
+
薄く公開する(いずれも既存実装を SSoT として共有)。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
21
|
+
from typing import Any, Callable, List, Mapping, Optional, Sequence, TypeVar
|
|
22
|
+
|
|
23
|
+
from .expr_eval import Scope, Value, evaluate
|
|
24
|
+
from .plan import run_plan # re-export(SSoT)
|
|
25
|
+
|
|
26
|
+
Expr = Any # 未評価の Expression IR ノード(primitive の被演算子)
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
R = TypeVar("R")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _scope(scope: Optional[Scope]) -> Scope:
|
|
32
|
+
return scope if scope is not None else {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── (a) Expression IR 全演算子の個別 primitive ────────────────────────────────
|
|
36
|
+
# 各関数は単一キーの IR ノードを組んで evaluate に委譲する(意味論 SSoT は evaluate 一本)。
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def ref(path: Sequence[str], scope: Optional[Scope] = None) -> Value:
|
|
40
|
+
"""参照。中間 null は NULL_REF。"""
|
|
41
|
+
return evaluate({"ref": list(path)}, _scope(scope))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ref_opt(path: Sequence[str], scope: Optional[Scope] = None) -> Value:
|
|
45
|
+
"""null 安全参照。中間 null は null 伝播。"""
|
|
46
|
+
return evaluate({"refOpt": list(path)}, _scope(scope))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def int_lit(literal: str, scope: Optional[Scope] = None) -> Value:
|
|
50
|
+
"""``{int:"..."}`` リテラル(i64 checked)。"""
|
|
51
|
+
return evaluate({"int": literal}, _scope(scope))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def float_lit(n: float, scope: Optional[Scope] = None) -> Value:
|
|
55
|
+
"""``{float:n}`` リテラル(NaN/Inf は NAN_OR_INF)。"""
|
|
56
|
+
return evaluate({"float": n}, _scope(scope))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def obj(fields: Mapping[str, Expr], scope: Optional[Scope] = None) -> Value:
|
|
60
|
+
"""object 構築(own key ``__proto__`` は FORBIDDEN_KEY)。値は Expression IR。"""
|
|
61
|
+
return evaluate({"obj": dict(fields)}, _scope(scope))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def arr(elems: Sequence[Expr], scope: Optional[Scope] = None) -> Value:
|
|
65
|
+
"""array 構築。要素は Expression IR。"""
|
|
66
|
+
return evaluate({"arr": list(elems)}, _scope(scope))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def add(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
70
|
+
"""加算(int×int / float×float checked。混在は TYPE_MISMATCH)。"""
|
|
71
|
+
return evaluate({"add": [a, b]}, _scope(scope))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def sub(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
75
|
+
"""減算。"""
|
|
76
|
+
return evaluate({"sub": [a, b]}, _scope(scope))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def mul(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
80
|
+
"""乗算。"""
|
|
81
|
+
return evaluate({"mul": [a, b]}, _scope(scope))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def neg(a: Expr, scope: Optional[Scope] = None) -> Value:
|
|
85
|
+
"""単項マイナス。"""
|
|
86
|
+
return evaluate({"neg": [a]}, _scope(scope))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def div(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
90
|
+
"""除算(常に float。|int|>2^53 は PRECISION_LOSS、0除算は NAN_OR_INF)。"""
|
|
91
|
+
return evaluate({"div": [a, b]}, _scope(scope))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def mod(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
95
|
+
"""剰余(truncated、符号は被除数。int の 0 剰余は MOD_ZERO)。"""
|
|
96
|
+
return evaluate({"mod": [a, b]}, _scope(scope))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def concat(parts: Sequence[Expr], scope: Optional[Scope] = None) -> Value:
|
|
100
|
+
"""文字列連結(n-ary, min 2。string のみ)。"""
|
|
101
|
+
return evaluate({"concat": list(parts)}, _scope(scope))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def eq(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
105
|
+
"""等価(同一スカラ型のみ。null==null 可)。"""
|
|
106
|
+
return evaluate({"eq": [a, b]}, _scope(scope))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def ne(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
110
|
+
"""非等価。"""
|
|
111
|
+
return evaluate({"ne": [a, b]}, _scope(scope))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def lt(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
115
|
+
"""小なり(同一型 int/float/string。string は code-point 順)。"""
|
|
116
|
+
return evaluate({"lt": [a, b]}, _scope(scope))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def le(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
120
|
+
"""以下。"""
|
|
121
|
+
return evaluate({"le": [a, b]}, _scope(scope))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def gt(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
125
|
+
"""大なり。"""
|
|
126
|
+
return evaluate({"gt": [a, b]}, _scope(scope))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def ge(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
130
|
+
"""以上。"""
|
|
131
|
+
return evaluate({"ge": [a, b]}, _scope(scope))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def and_(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
135
|
+
"""論理積(短絡: a=false なら b は評価されない)。b は未評価 IR で渡す。"""
|
|
136
|
+
return evaluate({"and": [a, b]}, _scope(scope))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def or_(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
140
|
+
"""論理和(短絡: a=true なら b は評価されない)。"""
|
|
141
|
+
return evaluate({"or": [a, b]}, _scope(scope))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def not_(a: Expr, scope: Optional[Scope] = None) -> Value:
|
|
145
|
+
"""論理否定(strict bool。truthiness なし)。"""
|
|
146
|
+
return evaluate({"not": [a]}, _scope(scope))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def coalesce(a: Expr, b: Expr, scope: Optional[Scope] = None) -> Value:
|
|
150
|
+
"""null 合体(左が null のときだけ右を評価)。右は未評価 IR で渡す。"""
|
|
151
|
+
return evaluate({"coalesce": [a, b]}, _scope(scope))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def cond(c: Expr, then: Expr, els: Expr, scope: Optional[Scope] = None) -> Value:
|
|
155
|
+
"""条件(strict bool。採用側のみ評価)。then/else は未評価 IR で渡す。"""
|
|
156
|
+
return evaluate({"cond": [c, then, els]}, _scope(scope))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def len_(a: Expr, scope: Optional[Scope] = None) -> Value:
|
|
160
|
+
"""配列長(配列のみ。string length は v1 に無い)。"""
|
|
161
|
+
return evaluate({"len": [a]}, _scope(scope))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def eval_expr(node: Expr, scope: Optional[Scope] = None) -> Value:
|
|
165
|
+
"""生の Expression IR を直接評価する汎用 primitive(意味論は個別 primitive と同一 SSoT)。"""
|
|
166
|
+
return evaluate(node, _scope(scope))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ── (c) bounded 並行プリミティブ(順序保存)─────────────────────────────────────
|
|
170
|
+
def map_with_concurrency(
|
|
171
|
+
items: Sequence[T],
|
|
172
|
+
concurrency: int,
|
|
173
|
+
worker: Callable[[T, int], R],
|
|
174
|
+
) -> List[R]:
|
|
175
|
+
"""入力順を保存し in-flight を ``concurrency`` で上限する bounded 並行 map。
|
|
176
|
+
|
|
177
|
+
execution-plan.md §4 の determinism 規律(output[i]=worker(items[i]) / 完了順非依存 /
|
|
178
|
+
ThreadPoolExecutor bounded dispatch)を単体 primitive として公開する。plan.py の並列経路と
|
|
179
|
+
同一規律(結果は index 順に確定するので逐次実行と一致)。
|
|
180
|
+
|
|
181
|
+
:param items: 入力(順序保存の基準)。
|
|
182
|
+
:param concurrency: 同時 in-flight 上限(<=1 で逐次)。
|
|
183
|
+
:param worker: 各要素の処理 ``worker(item, index)``。
|
|
184
|
+
:returns: ``items`` と同じ長さ・順序の結果リスト。
|
|
185
|
+
"""
|
|
186
|
+
n = len(items)
|
|
187
|
+
out: List[Optional[R]] = [None] * n
|
|
188
|
+
limit = max(1, int(concurrency))
|
|
189
|
+
if limit <= 1 or n <= 1:
|
|
190
|
+
for i in range(n):
|
|
191
|
+
out[i] = worker(items[i], i)
|
|
192
|
+
return out # type: ignore[return-value]
|
|
193
|
+
workers = min(limit, n)
|
|
194
|
+
with ThreadPoolExecutor(max_workers=workers) as pool:
|
|
195
|
+
futures = {pool.submit(worker, items[i], i): i for i in range(n)}
|
|
196
|
+
for fut, i in futures.items():
|
|
197
|
+
out[i] = fut.result() # 完了順非依存: index 固定スロットに書く
|
|
198
|
+
return out # type: ignore[return-value]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ── (d) skip / unproduced-value helper ────────────────────────────────────────
|
|
202
|
+
def unproduced_value(kind: Optional[str]) -> Value:
|
|
203
|
+
"""未生成 Port(Skip)の表現値。single→None / connection→空 connection。"""
|
|
204
|
+
return {"items": [], "cursor": None} if kind == "connection" else None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def skip_single() -> Value:
|
|
208
|
+
"""single relation の未生成 = None。"""
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def skip_connection() -> Value:
|
|
213
|
+
"""connection relation の未生成 = {items:[],cursor:None}。"""
|
|
214
|
+
return {"items": [], "cursor": None}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ── (e) hydration / relation helper ───────────────────────────────────────────
|
|
218
|
+
def hydrate_into(
|
|
219
|
+
over: Sequence[Value],
|
|
220
|
+
key: str,
|
|
221
|
+
kept_idx: Sequence[int],
|
|
222
|
+
values: Sequence[Value],
|
|
223
|
+
) -> List[Value]:
|
|
224
|
+
"""behavior.py の map.into と同一規律の zip-attach helper。
|
|
225
|
+
|
|
226
|
+
``over`` の各要素へ ``key`` で ``values[k]`` を書き戻した augment 済みリストを返す
|
|
227
|
+
(over と同じ長さ・順序、親不変=コピー、guard skip 要素は無変更で pass-through)。
|
|
228
|
+
|
|
229
|
+
:raises ValueError: augment 対象が dict でない場合(MAP_INTO_ELEMENT_NOT_OBJECT 相当)。
|
|
230
|
+
"""
|
|
231
|
+
augmented: List[Value] = []
|
|
232
|
+
k = 0
|
|
233
|
+
for i, el in enumerate(over):
|
|
234
|
+
if k < len(kept_idx) and kept_idx[k] == i:
|
|
235
|
+
if not isinstance(el, dict):
|
|
236
|
+
raise ValueError(
|
|
237
|
+
f"hydrate_into: element {i} is not an object (into requires object elements)"
|
|
238
|
+
)
|
|
239
|
+
augmented.append({**el, key: values[k]})
|
|
240
|
+
k += 1
|
|
241
|
+
else:
|
|
242
|
+
augmented.append(el)
|
|
243
|
+
return augmented
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
__all__ = [
|
|
247
|
+
"Expr",
|
|
248
|
+
# (a) expression operators
|
|
249
|
+
"ref",
|
|
250
|
+
"ref_opt",
|
|
251
|
+
"int_lit",
|
|
252
|
+
"float_lit",
|
|
253
|
+
"obj",
|
|
254
|
+
"arr",
|
|
255
|
+
"add",
|
|
256
|
+
"sub",
|
|
257
|
+
"mul",
|
|
258
|
+
"neg",
|
|
259
|
+
"div",
|
|
260
|
+
"mod",
|
|
261
|
+
"concat",
|
|
262
|
+
"eq",
|
|
263
|
+
"ne",
|
|
264
|
+
"lt",
|
|
265
|
+
"le",
|
|
266
|
+
"gt",
|
|
267
|
+
"ge",
|
|
268
|
+
"and_",
|
|
269
|
+
"or_",
|
|
270
|
+
"not_",
|
|
271
|
+
"coalesce",
|
|
272
|
+
"cond",
|
|
273
|
+
"len_",
|
|
274
|
+
"eval_expr",
|
|
275
|
+
# (b) run_plan (re-export)
|
|
276
|
+
"run_plan",
|
|
277
|
+
# (c) bounded concurrency
|
|
278
|
+
"map_with_concurrency",
|
|
279
|
+
# (d) skip / unproduced
|
|
280
|
+
"unproduced_value",
|
|
281
|
+
"skip_single",
|
|
282
|
+
"skip_connection",
|
|
283
|
+
# (e) hydration
|
|
284
|
+
"hydrate_into",
|
|
285
|
+
]
|
{behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: behavior-contracts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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 (
|
|
63
|
+
Runs the 7 shared suites (164 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 # → "
|
|
68
|
+
python -m behavior_contracts.conformance # → "164 passed, 0 failed / 164 vectors across 7 suites"
|
|
69
69
|
# or the console script:
|
|
70
70
|
behavior-contracts-conformance
|
|
71
71
|
```
|
{behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/SOURCES.txt
RENAMED
|
@@ -10,6 +10,7 @@ src/behavior_contracts/expr_eval.py
|
|
|
10
10
|
src/behavior_contracts/fingerprint.py
|
|
11
11
|
src/behavior_contracts/guard.py
|
|
12
12
|
src/behavior_contracts/plan.py
|
|
13
|
+
src/behavior_contracts/primitives.py
|
|
13
14
|
src/behavior_contracts/template.py
|
|
14
15
|
src/behavior_contracts.egg-info/PKG-INFO
|
|
15
16
|
src/behavior_contracts.egg-info/SOURCES.txt
|
|
@@ -20,4 +21,5 @@ src/behavior_contracts.egg-info/top_level.txt
|
|
|
20
21
|
tests/test_codegen_generated.py
|
|
21
22
|
tests/test_conformance.py
|
|
22
23
|
tests/test_plan_parallel.py
|
|
24
|
+
tests/test_primitives.py
|
|
23
25
|
tests/test_public_api.py
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""test_primitives.py — codegen primitive surface(bc#36)の pytest ユニットテスト。
|
|
2
|
+
|
|
3
|
+
各式演算子 primitive が「正常系 + fail-closed エッジ」で ``evaluate_expression``(SSoT)と
|
|
4
|
+
完全一致することを差分検証する。さらに map_with_concurrency(順序保存・bounded)/
|
|
5
|
+
unproduced helper / hydrate_into を検証する。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from behavior_contracts import (
|
|
16
|
+
ExprFailure,
|
|
17
|
+
evaluate_expression,
|
|
18
|
+
hydrate_into,
|
|
19
|
+
map_with_concurrency,
|
|
20
|
+
primitives as P,
|
|
21
|
+
skip_connection,
|
|
22
|
+
skip_single,
|
|
23
|
+
unproduced_value,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _same(actual, node, scope=None):
|
|
28
|
+
assert actual == evaluate_expression(node, scope or {})
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _both_fail(prim_call, node, scope=None):
|
|
32
|
+
with pytest.raises(ExprFailure) as pe:
|
|
33
|
+
prim_call()
|
|
34
|
+
with pytest.raises(ExprFailure) as ee:
|
|
35
|
+
evaluate_expression(node, scope or {})
|
|
36
|
+
assert pe.value.code == ee.value.code
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_literals_and_refs_match():
|
|
40
|
+
scope = {"a": {"b": 7}, "n": None}
|
|
41
|
+
_same(P.int_lit("42"), {"int": "42"})
|
|
42
|
+
_same(P.float_lit(3.5), {"float": 3.5})
|
|
43
|
+
_same(P.ref(["a", "b"], scope), {"ref": ["a", "b"]}, scope)
|
|
44
|
+
_same(P.ref_opt(["n", "x"], scope), {"refOpt": ["n", "x"]}, scope)
|
|
45
|
+
_same(P.obj({"x": {"int": "1"}}, scope), {"obj": {"x": {"int": "1"}}}, scope)
|
|
46
|
+
_same(P.arr([{"int": "1"}, {"int": "2"}], scope), {"arr": [{"int": "1"}, {"int": "2"}]}, scope)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_arithmetic_match_and_fail_closed():
|
|
50
|
+
_same(P.add({"int": "1"}, {"int": "2"}), {"add": [{"int": "1"}, {"int": "2"}]})
|
|
51
|
+
_same(P.sub({"int": "5"}, {"int": "3"}), {"sub": [{"int": "5"}, {"int": "3"}]})
|
|
52
|
+
_same(P.mul({"int": "4"}, {"int": "6"}), {"mul": [{"int": "4"}, {"int": "6"}]})
|
|
53
|
+
_same(P.neg({"int": "9"}), {"neg": [{"int": "9"}]})
|
|
54
|
+
_same(P.div({"int": "7"}, {"int": "2"}), {"div": [{"int": "7"}, {"int": "2"}]})
|
|
55
|
+
_same(P.mod({"int": "7"}, {"int": "3"}), {"mod": [{"int": "7"}, {"int": "3"}]})
|
|
56
|
+
|
|
57
|
+
_both_fail(
|
|
58
|
+
lambda: P.add({"int": "9223372036854775807"}, {"int": "1"}),
|
|
59
|
+
{"add": [{"int": "9223372036854775807"}, {"int": "1"}]},
|
|
60
|
+
)
|
|
61
|
+
_both_fail(lambda: P.mod({"int": "1"}, {"int": "0"}), {"mod": [{"int": "1"}, {"int": "0"}]})
|
|
62
|
+
_both_fail(lambda: P.add({"int": "1"}, {"float": 2.5}), {"add": [{"int": "1"}, {"float": 2.5}]})
|
|
63
|
+
_both_fail(
|
|
64
|
+
lambda: P.div({"int": "9007199254740993"}, {"int": "1"}),
|
|
65
|
+
{"div": [{"int": "9007199254740993"}, {"int": "1"}]},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_comparison_concat_bool_cond_len_match():
|
|
70
|
+
_same(P.eq({"int": "1"}, {"int": "1"}), {"eq": [{"int": "1"}, {"int": "1"}]})
|
|
71
|
+
_same(P.ne({"int": "1"}, {"int": "2"}), {"ne": [{"int": "1"}, {"int": "2"}]})
|
|
72
|
+
_same(P.lt("a", "b"), {"lt": ["a", "b"]})
|
|
73
|
+
_same(P.le("a", "a"), {"le": ["a", "a"]})
|
|
74
|
+
_same(P.gt({"int": "3"}, {"int": "2"}), {"gt": [{"int": "3"}, {"int": "2"}]})
|
|
75
|
+
_same(P.ge({"int": "2"}, {"int": "2"}), {"ge": [{"int": "2"}, {"int": "2"}]})
|
|
76
|
+
_same(P.concat(["x", "y", "z"]), {"concat": ["x", "y", "z"]})
|
|
77
|
+
_same(P.and_(True, False), {"and": [True, False]})
|
|
78
|
+
_same(P.or_(False, True), {"or": [False, True]})
|
|
79
|
+
_same(P.not_(True), {"not": [True]})
|
|
80
|
+
_same(P.coalesce(None, "fb"), {"coalesce": [None, "fb"]})
|
|
81
|
+
_same(P.cond(True, "t", "e"), {"cond": [True, "t", "e"]})
|
|
82
|
+
_same(P.len_({"arr": [True, False]}), {"len": [{"arr": [True, False]}]})
|
|
83
|
+
|
|
84
|
+
_both_fail(lambda: P.concat(["x", {"int": "1"}]), {"concat": ["x", {"int": "1"}]})
|
|
85
|
+
_both_fail(lambda: P.len_("nope"), {"len": ["nope"]})
|
|
86
|
+
_both_fail(lambda: P.cond({"int": "1"}, "t", "e"), {"cond": [{"int": "1"}, "t", "e"]})
|
|
87
|
+
_both_fail(lambda: P.ref(["n", "x"], {"n": None}), {"ref": ["n", "x"]}, {"n": None})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_short_circuit_preserved():
|
|
91
|
+
# {int:"bad"} would raise INVALID_LITERAL if evaluated.
|
|
92
|
+
assert P.and_(False, {"int": "bad"}) is False
|
|
93
|
+
assert P.or_(True, {"int": "bad"}) is True
|
|
94
|
+
assert P.cond(True, "ok", {"int": "bad"}) == "ok"
|
|
95
|
+
assert P.coalesce("present", {"int": "bad"}) == "present"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_eval_expr_same_ssot():
|
|
99
|
+
_same(P.eval_expr({"add": [{"int": "2"}, {"int": "3"}]}), {"add": [{"int": "2"}, {"int": "3"}]})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_map_with_concurrency_order_and_bound():
|
|
103
|
+
items = list(range(8))
|
|
104
|
+
in_flight = 0
|
|
105
|
+
max_in_flight = 0
|
|
106
|
+
lock = threading.Lock()
|
|
107
|
+
|
|
108
|
+
def worker(x, _i):
|
|
109
|
+
nonlocal in_flight, max_in_flight
|
|
110
|
+
with lock:
|
|
111
|
+
in_flight += 1
|
|
112
|
+
max_in_flight = max(max_in_flight, in_flight)
|
|
113
|
+
time.sleep(0.005)
|
|
114
|
+
with lock:
|
|
115
|
+
in_flight -= 1
|
|
116
|
+
return x * 10
|
|
117
|
+
|
|
118
|
+
out = map_with_concurrency(items, 3, worker)
|
|
119
|
+
assert out == [x * 10 for x in items] # order preserved
|
|
120
|
+
assert max_in_flight <= 3
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_map_with_concurrency_degenerate():
|
|
124
|
+
assert map_with_concurrency([1, 2, 3], 1, lambda x, _i: x + 1) == [2, 3, 4]
|
|
125
|
+
assert map_with_concurrency([], 4, lambda x, _i: x) == []
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_skip_unproduced_helpers():
|
|
129
|
+
assert unproduced_value("single") is None
|
|
130
|
+
assert unproduced_value("connection") == {"items": [], "cursor": None}
|
|
131
|
+
assert unproduced_value(None) is None
|
|
132
|
+
assert skip_single() is None
|
|
133
|
+
assert skip_connection() == {"items": [], "cursor": None}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_hydrate_into():
|
|
137
|
+
over = [{"id": 1}, {"id": 2}, {"id": 3}]
|
|
138
|
+
out = hydrate_into(over, "tag", [0, 2], ["A", "C"])
|
|
139
|
+
assert out == [{"id": 1, "tag": "A"}, {"id": 2}, {"id": 3, "tag": "C"}]
|
|
140
|
+
assert over[0] == {"id": 1} # parent not mutated
|
|
141
|
+
with pytest.raises(ValueError):
|
|
142
|
+
hydrate_into([42], "tag", [0], ["x"])
|
|
@@ -6,6 +6,7 @@ conformance vector とは独立に、公開 API を直接叩いて境界条件
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import json
|
|
9
10
|
import math
|
|
10
11
|
|
|
11
12
|
import pytest
|
|
@@ -22,6 +23,7 @@ from behavior_contracts import (
|
|
|
22
23
|
BehaviorFailure,
|
|
23
24
|
assert_portable,
|
|
24
25
|
assert_portable_component_graph,
|
|
26
|
+
assert_portable_type_notation,
|
|
25
27
|
canonical_json,
|
|
26
28
|
canonical_value,
|
|
27
29
|
cmp_code_points,
|
|
@@ -30,6 +32,7 @@ from behavior_contracts import (
|
|
|
30
32
|
encode_value,
|
|
31
33
|
evaluate_expression,
|
|
32
34
|
final_tree,
|
|
35
|
+
fingerprint_component_graph,
|
|
33
36
|
py_float_repr,
|
|
34
37
|
render_template,
|
|
35
38
|
resolve_partial,
|
|
@@ -642,3 +645,102 @@ class TestAssertPortableComponentGraph:
|
|
|
642
645
|
"output": {"obj": {"k": {"bogusOp": [1]}}}}]}
|
|
643
646
|
with pytest.raises(PortabilityError):
|
|
644
647
|
assert_portable_component_graph(bad)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# 型注記付き可搬 IR(B0 / bc#44)— MINI_IR に additive な outType/outputType を載せた変種。
|
|
651
|
+
# fingerprint pin 値は TS 参照(ts/test/generator.test.ts)/ Rust / Go / PHP と一致する。
|
|
652
|
+
_TYPED_MINI_IR = {
|
|
653
|
+
"irVersion": 1,
|
|
654
|
+
"exprVersion": 2,
|
|
655
|
+
"components": [
|
|
656
|
+
{
|
|
657
|
+
"name": "getA",
|
|
658
|
+
"inputPorts": {"id": {"type": "string", "required": True}},
|
|
659
|
+
"body": [
|
|
660
|
+
{
|
|
661
|
+
"id": "a",
|
|
662
|
+
"component": "GetItem",
|
|
663
|
+
"ports": {"PK": {"concat": ["A#", {"ref": ["id"]}]}},
|
|
664
|
+
"outType": {"obj": {"id": "string", "n": "int", "tags": {"arr": "string"}, "note": {"opt": "string"}}},
|
|
665
|
+
}
|
|
666
|
+
],
|
|
667
|
+
"output": {"ref": ["a"]},
|
|
668
|
+
"outputType": {"obj": {"id": "string", "n": "int", "tags": {"arr": "string"}, "note": {"opt": "string"}}},
|
|
669
|
+
"plan": {"groups": [[0]], "concurrency": 16},
|
|
670
|
+
}
|
|
671
|
+
],
|
|
672
|
+
}
|
|
673
|
+
_UNTYPED_MINI_IR = {
|
|
674
|
+
"irVersion": 1,
|
|
675
|
+
"exprVersion": 2,
|
|
676
|
+
"components": [
|
|
677
|
+
{
|
|
678
|
+
"name": "getA",
|
|
679
|
+
"inputPorts": {"id": {"type": "string", "required": True}},
|
|
680
|
+
"body": [{"id": "a", "component": "GetItem", "ports": {"PK": {"concat": ["A#", {"ref": ["id"]}]}}}],
|
|
681
|
+
"output": {"ref": ["a"]},
|
|
682
|
+
"plan": {"groups": [[0]], "concurrency": 16},
|
|
683
|
+
}
|
|
684
|
+
],
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
class TestPortableTypeNotation:
|
|
689
|
+
"""可搬型記法(B0 / bc#44 — scp-ir-architecture.md §5.2)の閉集合 + fail-closed。"""
|
|
690
|
+
|
|
691
|
+
def test_accepts_closed_set(self):
|
|
692
|
+
for t in ("string", "int", "float", "bool", "null"):
|
|
693
|
+
assert_portable_type_notation(t, "$")
|
|
694
|
+
assert_portable_type_notation({"opt": "string"}, "$")
|
|
695
|
+
assert_portable_type_notation({"arr": "int"}, "$")
|
|
696
|
+
assert_portable_type_notation(
|
|
697
|
+
{"obj": {"id": "string", "tags": {"arr": {"opt": "string"}}, "meta": {"obj": {"x": "bool"}}}}, "$"
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def test_fail_closed_variants(self):
|
|
701
|
+
for bad in [
|
|
702
|
+
"int32", # unknown scalar
|
|
703
|
+
"any", # no escape hatch
|
|
704
|
+
{"set": "int"}, # unknown compound kind
|
|
705
|
+
{"opt": "int", "arr": "int"}, # multi-key
|
|
706
|
+
{}, # zero-key
|
|
707
|
+
{"arr": "int64"}, # recursive unknown scalar
|
|
708
|
+
{"obj": ["string"]}, # obj value not a field->type object
|
|
709
|
+
42, # non-str / non-object
|
|
710
|
+
["string"], # array
|
|
711
|
+
]:
|
|
712
|
+
with pytest.raises(PortabilityError):
|
|
713
|
+
assert_portable_type_notation(bad, "$")
|
|
714
|
+
|
|
715
|
+
def test_fail_closed_proto_obj_type_field_key(self):
|
|
716
|
+
bad = json.loads('{"obj":{"__proto__":"string"}}')
|
|
717
|
+
with pytest.raises(PortabilityError, match="__proto__"):
|
|
718
|
+
assert_portable_type_notation(bad, "$")
|
|
719
|
+
|
|
720
|
+
def test_graph_accepts_additive_annotations(self):
|
|
721
|
+
assert_portable_component_graph(_TYPED_MINI_IR) # must not raise
|
|
722
|
+
|
|
723
|
+
def test_graph_backward_compat_outtype_absent(self):
|
|
724
|
+
# outType/outputType の無い IR は従来通り accept(B0 後方互換の pin)。
|
|
725
|
+
assert_portable_component_graph(_UNTYPED_MINI_IR) # must not raise
|
|
726
|
+
|
|
727
|
+
def test_graph_fail_closed_bad_outtype(self):
|
|
728
|
+
bad = {"irVersion": 1, "exprVersion": 2, "components": [
|
|
729
|
+
{"name": "x", "inputPorts": {}, "body": [
|
|
730
|
+
{"id": "a", "component": "GetItem", "ports": {}, "outType": "int32"}], "output": None}]}
|
|
731
|
+
with pytest.raises(PortabilityError) as ei:
|
|
732
|
+
assert_portable_component_graph(bad)
|
|
733
|
+
assert ei.value.path == "$.components[0].body[0].outType"
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
class TestTypedFingerprintParity:
|
|
737
|
+
"""型注記付き IR の fingerprint parity(B0 / bc#44)。TS/Rust/Go/PHP と同一 fnv1a64。"""
|
|
738
|
+
|
|
739
|
+
def test_typed_pinned_cross_language_value(self):
|
|
740
|
+
assert fingerprint_component_graph(_TYPED_MINI_IR) == "fnv1a64:c4d20ffa450dc313"
|
|
741
|
+
|
|
742
|
+
def test_untyped_unchanged_and_annotations_change_hash(self):
|
|
743
|
+
# 後方互換: untyped MINI_IR は従来値(TS/Go/Rust/PHP pin)。
|
|
744
|
+
assert fingerprint_component_graph(_UNTYPED_MINI_IR) == "fnv1a64:9421ef4d0840926a"
|
|
745
|
+
# 型注記の有無で fingerprint が変わる(改変検出)。
|
|
746
|
+
assert fingerprint_component_graph(_TYPED_MINI_IR) != fingerprint_component_graph(_UNTYPED_MINI_IR)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/requires.txt
RENAMED
|
File without changes
|
{behavior_contracts-0.2.0 → behavior_contracts-0.2.2}/src/behavior_contracts.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|