behavior-contracts 0.1.2__py3-none-any.whl

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.
@@ -0,0 +1,108 @@
1
+ """behavior-contracts — 多言語 IR-Runtime 共通層(Python 実装)。
2
+
3
+ runtime-boundary.md で COMMON と判定された「薄い核」プリミティブのみを公開する。
4
+ DynamoDB / graphddb 固有の概念(backend 実行・key resolution・retry Tuning・hydrate・
5
+ bundle 形式)は一切含まない。TS 参照実装 ``ts/src/*`` の意味論を完全一致で移植したもの。
6
+
7
+ 公開 COMMON プリミティブ(runtime-boundary.md §2.1):
8
+ - validate_envelope — spec-version fail-closed 検査
9
+ - evaluate_expression — expression-ir.md の規範評価
10
+ - render_template — ``{param}`` 束縛描画(strict)
11
+ - run_plan — 実行計画の骨格実行(stage / Skip 伝播 / Policy Kind)
12
+ - canonical_value / canonical_json / py_float_repr — 決定的正準直列化
13
+ - assert_portable — Portability Guard(IR 可搬性不変条件)
14
+ - decode_value / deep_equals — conformance runner adapter 共通部
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ # ── 対応する仕様バージョン(IR/vector の spec version。ライブラリ semver とは別管理)──
20
+ SPEC_VERSIONS = {
21
+ "expression": 1,
22
+ "template": 1,
23
+ "plan": 1,
24
+ "canonical": 1,
25
+ }
26
+
27
+ # validate_envelope に渡す graphddb 形式の既定 supported 版(``"<major>.<minor>"``)。
28
+ ENVELOPE_SPEC_VERSION = "1.1"
29
+
30
+ # ── expression(evaluate_expression) ──────────────────────────────────────────
31
+ from .expr_eval import ( # noqa: E402
32
+ ExprFailure,
33
+ cmp_code_points,
34
+ encode_value,
35
+ evaluate as evaluate_expression,
36
+ )
37
+
38
+ # ── template(render_template) ────────────────────────────────────────────────
39
+ from .template import ( # noqa: E402
40
+ TemplateFailure,
41
+ render_template,
42
+ resolve_partial,
43
+ )
44
+
45
+ # ── plan(run_plan) ───────────────────────────────────────────────────────────
46
+ from .plan import ( # noqa: E402
47
+ PlanFailure,
48
+ final_tree,
49
+ run_plan,
50
+ )
51
+
52
+ # ── canonical(canonical_value / canonical_json / py_float_repr) ─────────────
53
+ from .canonical import ( # noqa: E402
54
+ CanonicalFailure,
55
+ canonical_json,
56
+ canonical_value,
57
+ py_float_repr,
58
+ )
59
+
60
+ # ── envelope(validate_envelope) ──────────────────────────────────────────────
61
+ from .envelope import ( # noqa: E402
62
+ EnvelopeFailure,
63
+ validate_envelope,
64
+ )
65
+
66
+ # ── guard(Portability Guard) ────────────────────────────────────────────────
67
+ from .guard import ( # noqa: E402
68
+ PortabilityError,
69
+ assert_portable,
70
+ )
71
+
72
+ # ── conformance runner adapter 共通部 ─────────────────────────────────────────
73
+ from .codec import ( # noqa: E402
74
+ decode_value,
75
+ deep_equals,
76
+ )
77
+
78
+ __all__ = [
79
+ "SPEC_VERSIONS",
80
+ "ENVELOPE_SPEC_VERSION",
81
+ # expression
82
+ "evaluate_expression",
83
+ "encode_value",
84
+ "cmp_code_points",
85
+ "ExprFailure",
86
+ # template
87
+ "render_template",
88
+ "resolve_partial",
89
+ "TemplateFailure",
90
+ # plan
91
+ "run_plan",
92
+ "final_tree",
93
+ "PlanFailure",
94
+ # canonical
95
+ "canonical_value",
96
+ "canonical_json",
97
+ "py_float_repr",
98
+ "CanonicalFailure",
99
+ # envelope
100
+ "validate_envelope",
101
+ "EnvelopeFailure",
102
+ # guard
103
+ "assert_portable",
104
+ "PortabilityError",
105
+ # codec
106
+ "decode_value",
107
+ "deep_equals",
108
+ ]
@@ -0,0 +1,222 @@
1
+ """canonical.py — canonical-serialization.md の参照実装(Python port)。
2
+
3
+ TS 参照実装 ``ts/src/canonical.ts`` の意味論を完全一致で移植。3 プリミティブ:
4
+ - py_float_repr(n) : CPython repr(float) 相当の正準10進(parity SSoT = Python)
5
+ - canonical_value(v) : key identity(トップレベルのみキーソート) — §2 serializeContractKey
6
+ - canonical_json(v) : fingerprint(全階層キーソート) — §3 canonicalJson
7
+
8
+ 値モデルは expr_eval と共有(int / float / str / bool / None / list / dict)。
9
+ runtime 値の直列化:
10
+ - int → 素の 10 進整数(範囲外も 10 進で素通し。DynamoDB N 値層)
11
+ - float → py_float_repr(§4.1)。NaN/±Inf は Failure(§4.1-6)
12
+
13
+ **Python parity SSoT の要点**: py_float_repr は CPython の ``repr(float)`` を SSoT とする。
14
+ Python では ``repr(float)`` そのものが CPython の最短往復10進 + scientific 閾値
15
+ (decpt<=-4 || decpt>16) + 指数 2 桁 pad と一致する。したがって Python port は
16
+ ``repr()`` 準拠で実装し、TS 参照アルゴリズムと byte 一致することを検証する。
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import math
23
+ from typing import Any
24
+
25
+ Value = Any
26
+
27
+ _CANONICAL_FAILURE_CODES = frozenset({"NAN_OR_INF", "INVALID_VALUE"})
28
+
29
+
30
+ class CanonicalFailure(Exception):
31
+ """canonical 直列化の Failure(NAN_OR_INF / INVALID_VALUE)。"""
32
+
33
+ def __init__(self, code: str, message: str) -> None:
34
+ super().__init__(message)
35
+ self.code = code
36
+
37
+
38
+ def _fail(code: str, message: str) -> "None":
39
+ raise CanonicalFailure(code, message)
40
+
41
+
42
+ def _is_int(v: Value) -> bool:
43
+ return isinstance(v, int) and not isinstance(v, bool)
44
+
45
+
46
+ def _is_plain_object(v: Value) -> bool:
47
+ return isinstance(v, dict)
48
+
49
+
50
+ # ── §4.1 float の正準10進(CPython repr(float))────────────────────────────────
51
+ def py_float_repr(n: float) -> str:
52
+ """CPython の ``repr(float)`` / ``json.dumps(float)`` と byte 一致する 10 進表現。
53
+
54
+ 規則(canonical-serialization.md §4.1 / CPython format_float_short('r')):
55
+ 1. 最短往復 digits。
56
+ 2. scientific 判定: decpt <= -4 || decpt > 16 で指数表記。
57
+ 3. 指数は符号つき・最低 2 桁 zero-pad(e+05 / e-07)。
58
+ 4. 整数値 float は ``.0`` を保つ(123 → "123.0")。
59
+ 5. -0.0 は "-0.0"、+0.0 は "0.0"。
60
+ 6. NaN / ±Inf は Failure。
61
+
62
+ 実装: CPython の ``repr(float)`` は既にこの正準表現を生成するため、そこから
63
+ digits/decpt を導出し TS 参照実装 (formatFixed/formatScientific) と同一の分岐で
64
+ 組み立てる。これにより仕様の閾値ロジックを Python 側でも明示的に踏む。
65
+ """
66
+ if not isinstance(n, float):
67
+ # int が渡された場合など。仕様上 floatRepr は float のみ。
68
+ _fail("INVALID_VALUE", f"py_float_repr expects a float, got {type(n).__name__}")
69
+ if math.isnan(n) or math.isinf(n):
70
+ _fail("NAN_OR_INF", f"non-finite float cannot be serialized: {n}")
71
+
72
+ # §4.1-5: -0.0 を signbit で捕捉。
73
+ if n == 0.0:
74
+ return "-0.0" if math.copysign(1.0, n) < 0 else "0.0"
75
+
76
+ neg = n < 0
77
+ abs_n = abs(n)
78
+
79
+ digits, decpt = _shortest_digits(abs_n)
80
+
81
+ if decpt <= -4 or decpt > 16:
82
+ body = _format_scientific(digits, decpt)
83
+ else:
84
+ body = _format_fixed(digits, decpt)
85
+
86
+ return "-" + body if neg else body
87
+
88
+
89
+ def _shortest_digits(abs_n: float):
90
+ """正の有限 float → shortest round-trip digits と decpt(値 = 0.<digits> × 10^decpt)。
91
+
92
+ CPython の ``repr(float)`` が shortest round-trip を出すので、それを指数正規化して
93
+ digits と 10 進指数を取り出す(TS 実装が ``Number#toString`` から取り出すのと同じ手順)。
94
+ """
95
+ # repr は "123.0" / "1.5" / "1e-07" / "1.25e+30" 等(Python の正準表現)。
96
+ s = repr(abs_n)
97
+ e_idx = s.find("e")
98
+ if e_idx >= 0:
99
+ mantissa = s[:e_idx]
100
+ exp = int(s[e_idx + 1 :])
101
+ else:
102
+ mantissa = s
103
+ exp = 0
104
+
105
+ dot = mantissa.find(".")
106
+ if dot >= 0:
107
+ int_part = mantissa[:dot]
108
+ frac_part = mantissa[dot + 1 :]
109
+ else:
110
+ int_part = mantissa
111
+ frac_part = ""
112
+
113
+ all_digits = int_part + frac_part
114
+ # decpt(暫定) = 整数部桁数 + exp
115
+ decpt = len(int_part) + exp
116
+
117
+ # 先頭ゼロ除去("0025" → "25" で decpt を減らす)。
118
+ lead = 0
119
+ while lead < len(all_digits) - 1 and all_digits[lead] == "0":
120
+ lead += 1
121
+ if lead > 0:
122
+ all_digits = all_digits[lead:]
123
+ decpt -= lead
124
+
125
+ # 末尾ゼロ除去(有効数字のみ)。
126
+ all_digits = all_digits.rstrip("0")
127
+ if all_digits == "":
128
+ return "0", 1
129
+ return all_digits, decpt
130
+
131
+
132
+ def _format_fixed(digits: str, decpt: int) -> str:
133
+ """固定小数点表記。整数値 float は ``.0`` を付ける(§4.1-4)。"""
134
+ if decpt <= 0:
135
+ return "0." + "0" * (-decpt) + digits
136
+ if decpt >= len(digits):
137
+ return digits + "0" * (decpt - len(digits)) + ".0"
138
+ return digits[:decpt] + "." + digits[decpt:]
139
+
140
+
141
+ def _format_scientific(digits: str, decpt: int) -> str:
142
+ """指数表記。d.ddde±XX(指数は最低 2 桁 zero-pad)(§4.1-2/3)。"""
143
+ first = digits[0]
144
+ rest = digits[1:]
145
+ mant = f"{first}.{rest}" if rest else first
146
+ e = decpt - 1
147
+ sign = "-" if e < 0 else "+"
148
+ mag = str(abs(e)).rjust(2, "0")
149
+ return f"{mant}e{sign}{mag}"
150
+
151
+
152
+ # ── §2 canonical_value(serialize_contract_key)— トップレベルのみキーソート ──────
153
+ def canonical_value(v: Value) -> str:
154
+ """key identity 用の正準直列化。
155
+
156
+ トップレベル object のキーのみ昇順ソートし、compact JSON を返す。ネスト値は挿入順のまま
157
+ (canonical-serialization.md §2)。
158
+ """
159
+ if _is_plain_object(v):
160
+ keys = sorted(v.keys(), key=_code_point_sort_key)
161
+ parts = [f"{_json_string(k)}:{_encode_nested(v[k])}" for k in keys]
162
+ return "{" + ",".join(parts) + "}"
163
+ return _encode_nested(v)
164
+
165
+
166
+ # ── §3 canonical_json — 全階層キーソート ────────────────────────────────────────
167
+ def canonical_json(v: Value) -> str:
168
+ """fingerprint 用。全 object 階層のキーを昇順ソート(配列は順序保持)。"""
169
+ if _is_plain_object(v):
170
+ keys = sorted(v.keys(), key=_code_point_sort_key)
171
+ parts = [f"{_json_string(k)}:{canonical_json(v[k])}" for k in keys]
172
+ return "{" + ",".join(parts) + "}"
173
+ if isinstance(v, list):
174
+ return "[" + ",".join(canonical_json(e) for e in v) + "]"
175
+ return _scalar_json(v)
176
+
177
+
178
+ # ── 内部: ネスト値の直列化(canonical_value 用。ネスト object は挿入順)──────────────
179
+ def _encode_nested(v: Value) -> str:
180
+ if _is_plain_object(v):
181
+ parts = [f"{_json_string(k)}:{_encode_nested(v[k])}" for k in v.keys()]
182
+ return "{" + ",".join(parts) + "}"
183
+ if isinstance(v, list):
184
+ return "[" + ",".join(_encode_nested(e) for e in v) + "]"
185
+ return _scalar_json(v)
186
+
187
+
188
+ def _scalar_json(v: Value) -> str:
189
+ """スカラ(int/float/str/bool/None)の JSON 直列化。float は py_float_repr。"""
190
+ if v is None:
191
+ return "null"
192
+ if isinstance(v, bool):
193
+ return "true" if v else "false"
194
+ if isinstance(v, str):
195
+ return _json_string(v)
196
+ if _is_int(v):
197
+ return str(v) # int: 素の 10 進整数(値層 JSON number)
198
+ if isinstance(v, float):
199
+ return py_float_repr(v) # float: CPython repr(§4.1)
200
+ _fail("INVALID_VALUE", "cannot serialize value of unknown type")
201
+
202
+
203
+ def _json_string(s: str) -> str:
204
+ """文字列を JSON 直列化(ensure_ascii=False = 非 ASCII 素通し)。
205
+
206
+ TS の ``JSON.stringify`` と一致させるため、Python の ``json.dumps`` を
207
+ ``ensure_ascii=False`` で使う(compact separators は文字列単体では無関係)。
208
+ """
209
+ return json.dumps(s, ensure_ascii=False)
210
+
211
+
212
+ def _code_point_sort_key(s: str):
213
+ """Unicode code-point 順のソートキー(正準キーソートの規範。全言語共通)。
214
+
215
+ canonical-serialization.md §2/§6: オブジェクトキーのソートは **code-point 順** が規範
216
+ (expression-ir.md §6 の文字列比較 = code-point 順と整合。UTF-16 code unit 順ではない —
217
+ astral-plane(U+10000+)キーで両者は食い違う)。Python の ``str`` はタプル化した
218
+ code point 列で比較されるので、ここでは code point のタプルを返す(``sorted`` の既定
219
+ ``str`` 比較と同一だが、規範が code-point であることを明示するための明示キー)。
220
+ BMP 内では code-point == UTF-16 なので既存 vector は不変。
221
+ """
222
+ return [ord(ch) for ch in s]
@@ -0,0 +1,88 @@
1
+ """codec.py — conformance runner adapter の共通部(Python port)。
2
+
3
+ TS 参照実装 ``ts/src/codec.ts`` の意味論を移植。golden vector の型付き値エンコード
4
+ (``{int:"..."}`` / ``{float:n}`` / ``{nan}`` / ``{inf}`` / 素の JSON number 分類)と
5
+ runtime 値の相互変換、および int/float を型で区別する深い等価判定。全言語 runner が
6
+ 同じ判定規則で同じ pass/fail を出すための共有ロジック(PROTOCOL.md §4)。
7
+
8
+ **Python の int/float 分類の要点**: Python の ``json.load`` は整数値の JSON number を
9
+ ``int``、小数部ありを ``float`` として復元する(TS のように精度損失しない)。したがって
10
+ ``decode_value`` は「Python の型をそのまま尊重しつつ ``{int}``/``{float}``/``{nan}``/``{inf}``
11
+ ラッパを解く」だけでよい。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import math
17
+ from typing import Any
18
+
19
+ Value = Any
20
+
21
+
22
+ def _is_int(v: Value) -> bool:
23
+ return isinstance(v, int) and not isinstance(v, bool)
24
+
25
+
26
+ def decode_value(x: Any) -> Value:
27
+ """golden vector の JSON 値表現を runtime 値へ復元する。
28
+
29
+ - 素の JSON number: 整数値 → int、小数部あり → float(Python の json が既に区別)。
30
+ - ``{int:"..."}`` → int(安全整数域外も正確)。
31
+ - ``{float:n}`` → float(整数値 float を明示)。
32
+ - ``{nan}`` / ``{inf: ±1}`` → NaN / ±Inf。
33
+ """
34
+ if x is None or isinstance(x, bool) or isinstance(x, str):
35
+ return x
36
+ if _is_int(x):
37
+ return int(x)
38
+ if isinstance(x, float):
39
+ return x
40
+ if isinstance(x, list):
41
+ return [decode_value(e) for e in x]
42
+ if isinstance(x, dict):
43
+ keys = list(x.keys())
44
+ if len(keys) == 1:
45
+ k = keys[0]
46
+ if k == "int" and isinstance(x["int"], str):
47
+ return int(x["int"])
48
+ if k == "float" and isinstance(x["float"], (int, float)) and not isinstance(
49
+ x["float"], bool
50
+ ):
51
+ return float(x["float"])
52
+ if k == "nan":
53
+ return float("nan")
54
+ if k == "inf":
55
+ return float("-inf") if x["inf"] < 0 else float("inf")
56
+ return {k: decode_value(v) for k, v in x.items()}
57
+ raise ValueError(f"cannot decode value: {x!s}")
58
+
59
+
60
+ def deep_equals(a: Value, b: Value) -> bool:
61
+ """int/float を型で区別する深い等価(PROTOCOL.md §4)。
62
+
63
+ int と float は等しくない。NaN は NaN と等しいとみなす(Failure 判定用)。
64
+ """
65
+ if a is None or b is None:
66
+ return a is b
67
+ # bool は int の前に判定(bool ≠ int)。
68
+ if isinstance(a, bool) or isinstance(b, bool):
69
+ return isinstance(a, bool) and isinstance(b, bool) and a == b
70
+ if _is_int(a) or _is_int(b):
71
+ return _is_int(a) and _is_int(b) and a == b
72
+ if isinstance(a, float) or isinstance(b, float):
73
+ if not (isinstance(a, float) and isinstance(b, float)):
74
+ return False
75
+ return a == b or (math.isnan(a) and math.isnan(b))
76
+ if isinstance(a, str) or isinstance(b, str):
77
+ return isinstance(a, str) and isinstance(b, str) and a == b
78
+ if isinstance(a, list):
79
+ if not isinstance(b, list) or len(a) != len(b):
80
+ return False
81
+ return all(deep_equals(x, y) for x, y in zip(a, b))
82
+ if isinstance(b, list):
83
+ return False
84
+ if isinstance(a, dict) and isinstance(b, dict):
85
+ if set(a.keys()) != set(b.keys()):
86
+ return False
87
+ return all(deep_equals(a[k], b[k]) for k in a)
88
+ return False